DEV Community

Arsalan Ahmed Yaldram
Arsalan Ahmed Yaldram

Posted on

Setup express with Typescript - querying database adding services

Introduction

In this series we will setup an express server using Typescript, we will be using TypeOrm as our ORM for querying a PostgresSql Database, we will also use Jest and SuperTest for testing. The goal of this series is not to create a full-fledged node backend but to setup an express starter project using typescript which can be used as a starting point if you want to develop a node backend using express and typescript.

Overview

This series is not recommended for beginners some familiarity and experience working with nodejs, express, typescript and typeorm is expected. In this post which is part four of our series we will : -

  • Use the Todo entity to query our database.
  • Create a Todo Service.
  • Use the Todo Service in our Todo controller.

Step One: Query our database

In our todo.controller.ts lets now remove the hard-coded todos and query our database like so :

import { Request, Response } from 'express';

import { AppDataSource } from '../../datasource';
import { Todos } from './todos.entity';

class TodoController {
  async getAllTodos(req: Request, res: Response) {
    try {
      const todos = await AppDataSource.getRepository(Todos)
        .createQueryBuilder('todos')
        .getMany();
      return res.status(200).json({
        status: true,
        statusCode: 200,
        todos,
      });
    } catch (error) {
      return res.status(500).json({
        status: false,
        statusCode: 500,
        message: 'Something unusual Happened',
      });
    }
  }

  async getTodoById(req: Request, res: Response) {
    try {
      const { id } = req.params;
      const todo = await AppDataSource.getRepository(Todos)
        .createQueryBuilder('todos')
        .where('todos.id = :id', { id })
        .getOne();
      if (!todo) {
        return res.status(404).json({
          status: false,
          statusCode: 404,
          message: `todo not found for id - ${id}`,
        });
      }

      return res.status(200).json({
        status: true,
        statusCode: 200,
        todo,
      });
    } catch (error) {
      return res.status(500).json({
        status: false,
        statusCode: 500,
        message: 'Something unusual Happened',
      });
    }
  }
}

export const todosController = new TodoController();
Enter fullscreen mode Exit fullscreen mode

For querying the database I use the queryBuilder pattern, the createQueryBuilder function takes in the table name, make sure you pass in the right name. The try-catch blocks are necessary to handle errors, thrown by our code, or by the orm library, etc. in the up coming tutorials we will create an async handler middleware, that will take care of all this, and then we can get rid of all these try-catch blocks.

Step 2: Setup Todo service

Writing all our queries and database related logic in separate service methods as opposed to writing them in the controller is one of the best practices:

  • Writing code that does one job (an atomic unit of code) is easily testable, readable and scalable in the long run.
  • We have re-usable functions that can be used in many controllers, other services throughout the project.
  • When testing our code, we can easily mock our services, as opposed to mocking our request handlers. We can also unit test our service functions.

You can notice in each controller code we have to call the getRepository function we can just move this piece of code in the entity file itself, or outside our controller class. But given the fact that we are using Typescript lets create a BaseService class. Under src/utils create a new file BaseService.ts and paste the following :

import { EntityTarget, Repository } from 'typeorm';

import { AppDataSource } from '../datasource';

export class BaseService<T> {
  repository: Repository<T>;

  constructor(entity: EntityTarget<T>) {
    this.repository = AppDataSource.getRepository(entity);
  }
}
Enter fullscreen mode Exit fullscreen mode

BaseService is a generic class, its sole job is to create a repository for us, so that every service class that extends it has the this.respository property. As our project grows we can move a lot of repetitive code in this base class. Now lets create our todos.service.ts inside src/api/todos.service.ts :

import { BaseService } from '../../utils/BaseService';

import { Todos, TodoStatus } from './todos.entity';

class TodosService extends BaseService<Todos> {
  constructor() {
    super(Todos);
  }

  getAllTodos() {
    return this.repository.createQueryBuilder('todos').getMany();
  }

  getTodoById(todoId: string) {
    return this.repository
      .createQueryBuilder('todos')
      .where('todos.id = :id', { id: todoId })
      .getOne();
  }

  createTodo(text: string, status: TodoStatus) {
    return this.repository
      .createQueryBuilder()
      .insert()
      .into(Todos)
      .values({
        text,
        status,
      })
      .returning('*')
      .execute();
  }
}

export const todosService = new TodosService();
Enter fullscreen mode Exit fullscreen mode

In our constructor we call super(Todo) this invokes the BaseService class constructor and passes our entity to it. We have also added a new createTodo function in our service.

Step Three: Use service in our controller

Now we will use our service methods in the controller file:

import { Request, Response } from 'express';

import { todosService } from './todos.service';

class TodoController {
  async getAllTodos(req: Request, res: Response) {
    try {
      const todos = await todosService.getAllTodos();
      return res.status(200).json({
        status: true,
        statusCode: 200,
        todos,
      });
    } catch (error) {
      return res.status(500).json({
        status: false,
        statusCode: 500,
        message: 'Something unusual Happened',
      });
    }
  }

  async getTodoById(req: Request, res: Response) {
    try {
      const { id: todoId } = req.params;
      const todo = await todosService.getTodoById(todoId);
      if (!todo) {
        return res.status(404).json({
          status: false,
          statusCode: 404,
          message: `todo not found for id - ${todoId}`,
        });
      }

      return res.status(200).json({
        status: true,
        statusCode: 200,
        todo,
      });
    } catch (error) {
      return res.status(500).json({
        status: false,
        statusCode: 500,
        message: 'Something unusual Happened',
      });
    }
  }

  async createTodo(req: Request, res: Response) {
    try {
      const { text, status } = req.body;
      const newTodo = await todosService.createTodo(text, status);
      return res.status(201).json({
        status: true,
        statusCode: 201,
        todo: newTodo.raw,
      });
    } catch (error) {
      return res.status(500).json({
        status: false,
        statusCode: 500,
        message: 'Something unusual Happened',
      });
    }
  }
}

export const todosController = new TodoController();
Enter fullscreen mode Exit fullscreen mode

Lets register an endpoint in our todos.router.ts:

 addRoutes(): void {
    this.router.get('/', todosController.getAllTodos);
    this.router.post('/', todosController.createTodo);
    this.router.get('/:id', todosController.getTodoById);
 }
Enter fullscreen mode Exit fullscreen mode

Now you can start your webserver by running npm run dev and test your endpoints. I personally use postman, but for such small projects I use REST client for vscode and here is my api.http -

GET http://localhost:8080/api/todos

###
GET http://localhost:8080/api/todos/:todoId

###
POST http://localhost:8080/api/todos 
Content-Type: application/json

{
  "text": "Walk for 30 minutes",
  "status": "pending"
}
Enter fullscreen mode Exit fullscreen mode

Summary

Well then we have finished setting up our service and we also used it in our controller. All the code for this tutorial can be found under the feat/querying-database branch here. In the next tutorial we will get rid of all the try and catch repetitive code by setting a global Error handler and an async middleware until next time PEACE.

Top comments (4)

Collapse
 
moonseeker profile image
Moon Seeker

Is there a way to select from the query builder just the fields the client selected?
E.g. if the client wants just the “type” from the todo entity, but not the text, then we shouldn’t select the entire object from the Db

Collapse
 
yaldram profile image
Arsalan Ahmed Yaldram

Hey thanks for your comment, yes we do have the .select method on the querybuilder. You can check the documentation here - orkhan.gitbook.io/typeorm/docs/sel...

Collapse
 
moonseeker profile image
Moon Seeker

Thanks, but that is not what I meant.
If you have a query that selects the user, and does an inner join on an entity ‘profile’ that query will always return both the user and the profile. Regardless of what the client actually want’s.

What I want to do is make sure that if the client only wants the user.id, then the server doesn’t fetch the entire user object + the join from the databases

Thread Thread
 
yaldram profile image
Arsalan Ahmed Yaldram

I would create two separate endpoints for such a use case. OR Introduce a field param like directus - docs.directus.io/reference/query/#... and see if the profile field exists if yes then call the join query, else just run a simple query on the user entity without a join, I would only do this field param filtering if there is no other solution.

And for this decision I would also consider the number of times this API is been called, if it is low then fine I will go ahead with the join query and whenever I need the user.id in my client I will take it from the redux state / client state as opposed to fetching it again. If the app has user login and all auth related stuff.

If say in your app, you are fetching a list of users and there are 2 pages, one needs all the join data and other just the user name and id, we can say fetch all the user info with join once store it in our client / redux state and manipulate the data for both the pages. Else have 2 separate endpoints, in my opinion its totally fine having 2 separate endpoints.