DEV Community

Cover image for Writing Scrum Toolkit #3 - Server with Node, TypeScript, Websocket and TypeORM
Meat Boy
Meat Boy

Posted on

5 3

Writing Scrum Toolkit #3 - Server with Node, TypeScript, Websocket and TypeORM

In the last article, we cover the setup for the client-side of the application. Today we are going to look closely at server-side part. 🚀

Application API is written with Express framework for file serving and Websocket for communication. Entry file for server:

// ...

dotenv.config();
const port = process.env.PORT;
const app: Express = express();
const server = http.createServer(app);

app.use(express.static(path.join(__dirname, 'public')));
app.get('(/*)?', async (req, res, next) => {
  res.sendFile(path.join(__dirname, 'public', 'index.html'));
});

AppDataSource.initialize().then(async () => {
  console.info('Database connected');
}).catch((error) => {
  console.error(error);
});

const io = new Server<IncomingEvents, OutgoingEvents, {}, User>(server, {
  transports: ['websocket', 'polling'],
});

io.on('connection', (socket: Socket<IncomingEvents, OutgoingEvents, {}, User>) => {
  registerUsersHandlers(io, socket);
  registerCardsHandlers(io, socket);
  registerBoardsHandlers(io, socket);
});

server.listen(port, () => {
  // eslint-disable-next-line no-console
  console.log(`Server is running at http://localhost:${port}`);
});

Enter fullscreen mode Exit fullscreen mode

You can see we register events similar to what we did on client-side. That is because we are using the same set of events.

export type IncomingUsersEvents = {
  Join: (data: {boardId: string, nickname: string; avatar: number;}) => void;
  SetSelectedPlanningCard: (data: {selectedPlanningCard: number}) => void;
  ToggleReady: () => void;
  ChangeUserData: (data: {nickname: string, avatar: number}) => void;
}

export type OutgoingUsersEvents = {
  Joined: (data: {
    localUser: RawUser,
    users: RawUser[],
    cards: RawCard[],
    board: {id: string, stage: number, maxVotes: number, timerTo: number, mode: string},
  }) => void;
  UserState: (data: {user: RawUser}) => void;
  UsersState: (data: {users: RawUser[]}) => void;
}

export type IncomingCardsEvents = {
  CreateCard: (data: {content: string, column: number}) => void;
  UpdateCard: (data: {cardId: string, content: string}) => void;
  DeleteCard: (data: {cardId: string}) => void;
  GetCards: () => void;
  GroupCards: (data: {cardId: string, stackedOn: string}) => void;
  UngroupCards: (data: {cardId: string}) => void;
  UpvoteCard: (data: {cardId: string}) => void;
  DownvoteCard: (data: {cardId: string}) => void;
}

export type OutgoingCardsEvents = {
  CardState: (data: {card: RawCard}) => void;
  DeleteCard: (data: {cardId: string}) => void;
  CardsState: (data: {cards: RawCard[]}) => void;
}

export type IncomingBoardsEvents = {
  SetTimer: (data: {duration: number}) => void;
  SetBoardMode: (data: { mode: string }) => void;
  SetMaxVotes: (data: {maxVotes: number}) => void;
  SetStage: (data: {stage: number}) => void;
}

export type OutgoingBoardsEvents = {
  BoardConfig: (data: {board: {
    stage: number,
      timerTo: number,
      maxVotes: number,
      mode: string,
  }}) => void;
}

export type IncomingEvents = IncomingUsersEvents & IncomingCardsEvents & IncomingBoardsEvents;
export type OutgoingEvents = OutgoingUsersEvents & OutgoingCardsEvents & OutgoingBoardsEvents;
Enter fullscreen mode Exit fullscreen mode

And handlers are using these events as follows:

// ...

const registerCardsHandlers = (
  io: Server<IncomingEvents, OutgoingEvents, {}, User>,
  socket: Socket<IncomingEvents, OutgoingEvents, {}, User>,
) => {
  socket.on('CreateCard', async ({ content, column }) => {
    try {
      if (Joi.string().min(1).max(512).validate(content).error) {
        console.error(`CreateCard: Invalid content: ${content}`);
        return;
      }

      if (Joi.number().allow(0, 1, 2).validate(column).error) {
        console.error(`CreateCard: Invalid column: ${column}`);
        return;
      }

      const card = await Cards.create({
        content,
        column,
        board: {
          id: socket.data.boardId,
        },
        user: {
          id: socket.data.userId,
        },
        stackedOn: '',
        votes: [],
      }).save();

      io.to(socket.data.boardId || '')
        .emit('CardState', { card: getRawCard(card) });
    } catch (error) {
      console.error(error);
    }
  });
// ...

export default registerCardsHandlers;
Enter fullscreen mode Exit fullscreen mode

To communicate with the backend server is using TypeORM. Initially, it has been connecting to Postgres but for my purpose, it was overkill so I switched to SQLite which is faster to provision, develop and maintain in this small app. If you want to switch back to Postgres it's just changing a few lines in the dataSource config.

import { DataSource } from 'typeorm';
import dotenv from 'dotenv';
import Boards from './Boards';
import Cards from './Cards';
import Users from './Users';
import Votes from './Votes';

dotenv.config();

const AppDataSource = new DataSource({
  type: 'sqlite',
  database: './db.sqlite',
  synchronize: true,
  logging: true,
  entities: [Boards, Cards, Users, Votes],
  subscribers: [],
  migrations: [],
});

export default AppDataSource;
Enter fullscreen mode Exit fullscreen mode

Models are simple entity classes that are extended TypeORM BaseEntity with some pre-made static methods to create and execute SQL queries.

export enum BoardMode {
  RETRO= 'retro',
  PLANNING_HIDDEN = 'planning_hidden',
  PLANNING_REVEALED = 'planning_revealed',
}

@Entity()
export default class Boards extends BaseEntity {
  @PrimaryGeneratedColumn('uuid')
    id: string;

  @OneToMany(() => Cards, (card) => card.board)
    cards: Cards[];

  @OneToMany(() => Users, (user) => user.board)
    users: Users[];

  @Column({
    type: 'integer',
    name: 'stage',
  })
    stage: number;

  @Column({
    type: 'integer',
    name: 'max_votes',
  })
    maxVotes: number;

  @Column({
    type: 'varchar',
    name: 'mode',
  })
    mode: string;

  @Column({
    name: 'timer_to',
  })
    timerTo: Date;

  @CreateDateColumn({
    name: 'created_at',
  })
    createdAt: Date;

  @UpdateDateColumn({
    name: 'updated_at',
  })
    updatedAt: Date;
}
Enter fullscreen mode Exit fullscreen mode

TypeORM was a great choice for small API servers to use. And in compare to Sequelize it has much clear syntax and works pretty well with TypeScript.

Top comments (0)

Visualizing Promises and Async/Await 🤯

async await

Learn the ins and outs of Promises and Async/Await!

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay