|

NestJS - Work better and more efficiently with Node.js

Those who want to start with Node.js might be a bit skeptical about the architecture at the beginning. And even those who are already working with Node.js might ask themselves at first: "How should I structure my project?", "What does a good setup look like?" or "How can I create services more efficiently?". I have a suitable answer to these questions: NestJS.

Brief overview of NestJS

  • Framework
  • Angular architecture
  • Simplifies Node.js and the Express framework
  • TypeScript support
  • good for REST APIs
  • good for GraphQL APIs
  • good for implementation with websockets (more on this in a later blog post)
  • good documentation
  • Swagger implemented
  • is not only liked by me:

Tweet - Backend frameworks with most "stars" on GitHub[1]

This article will primarily look at developing a REST API with NestJS. For those who would like to get started with NestJS or delve deeper into the topics touched upon here, I encourage you to read the documentation of NestJS.

Background

NestJS is a framework that builds one or actually two abstraction layers on top of Node.js and the well-known Express framework. Thus, there is no magic behind NestJS, but nevertheless the given architecture and facade allows a better structuring of the project and a more efficient workflow. As NestJS itself says, "The architecture is heavily inspired by Angular". You get that same feeling when you work with NestJS. Angular developers quickly become familiar with NestJS and those who know NestJS to some extent will also get into Angular faster.

Basic knowledge and folder structure .

In NestJS there are moduless similar to namespaces in the .NET world or packages in the Java world. Inside a module are mainly classes that belong together. A function of a Controller represents a route and takes care of passing it to a Provider (usually "Service"). A service then contains the actual logic and returns a value to the controller if necessary. A classic interface or a "normal" class is used as a model. To separate the business logic even better there are Guards, Middlewaress and Pipes. These three make sure that something is validated or checked during a request. Only then the actual route is reached. A Guard "protects" certain routes. I.e. it checks the request for authorization. A well known and (probably) most used example would be an authentication guard. Middlewares can also perform authentication (standard with the Express framework), but can still set values or similar. Pipe `s ensure that parameters or query components are validated or transformed.

These classes result in the following folder structure (example of a card game of mine):

  • src/
    • main.ts
    • app.module.ts
    • v1/
      • database/
        • database.module.ts
        • database.service.ts
      • cards/
        • dto/
          • ...
        • card.controller.ts
        • card.module.ts
        • card.service.ts
      • game/
        • dto/
          • ...
        • deck.service.ts
        • end-turn.service.ts
        • game.controller.ts
        • game.module.ts
        • game.service.ts
      • middlewares/
        • player-in-room.middleware.ts
        • set-user-agent.middleware.ts
        • ...
      • guards/
        • auth.guard.ts
      • ...

The folder structure is also maintained/preset using the CLI of NestJS.

Programming

With the basic knowledge from Basiswissen (Guards, Middlewares and Pipes are even optional) you can already start programming. Most things are understood anyway only in practice and consolidate there ;)

But here we mainly look at Controllers. Providers and Module`s are relatively uninteresting. A module contains only imports and applies middlewares to routes. A provider is a "normal" class, but it has a decorator.

An important part of NestJS are the decorators. Whether it is a module, controller or provider is determined by them.

@Controller('ue') // 'ue' as route prefix (optional)
export class UEController {
  constructor(private readonly ueService: UEService) {}

  @Get('hello') // creates a GET route. Addressable via GET 'ue/hello'.
  		// 'ue' as addition by the prefix above
  async sayHello(): Promise<string> {
    return await this.ueService.giveMeAHello();
  }
}

The Controller decorator makes the class recognized as a controller. Each function can have other decorators to make it recognized as a route. For example, the GET route "ue/hello" exists, which is accessible via the browser (if UEController is imported in the app module). The complete business logic can now be implemented in TypeScript or JavaScript in the service.

Now there is not only a GET decorator, but of course also POST, PUT and DELETE. However, it now becomes more difficult to test these in the browser. For this reason:

Swagger, Swagger, Swagger.

NestJS makes it very easy to build a Swagger documentation on the side. All it takes is a small adjustment to the main.ts file - the entry point of NestJS.

// main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Swagger
  const options = new DocumentBuilder()
    .setTitle('UE API')
    .setDescription('...')
    .setVersion('1.0')
    .addTag('v1')
    .setContact('UEBERBIT GmbH', 'https://www.ueberbit.de/', 'contact [at] ueberbit.de')
    .addBearerAuth(); // optional if any route has bearer authentication
  
  const doc = options.build();

  const document = SwaggerModule.createDocument(app, doc);
  SwaggerModule.setup('/api/v1/', app, document);

  await app.listen(PORT);

  console.log(`Listening on port ${PORT}`);
}
bootstrap();

This alone is enough for the following output
Standard output in Swagger without decorators

With the use of additional decorators it is possible to make Swagger even more informative.

@Controller('ue') // 'ue' as route prefix (optional)
export class UEController {
  constructor(private readonly ueService: UEService) {}

  @Get('hello') // creates a GET route. Addressable via GET 'ue/hello'.
                // 'ue' as addition by the prefix above
  @ApiOperation({
    summary: 'operation to get a string',
    description:
      'This operation allows to get a string with meaningful content',
  })
  @ApiResponse({
    status: 201,
    description: 'Returns a "Hello UEBERBIT" on success',
  })
  @ApiInternalServerErrorResponse({
    description:
      'This error is thrown when the service divides by zero.'
  })
  async sayHello(): Promise<string> {
    return this.ueService.giveMeAHello();
  }
}

Swagger output with decorators

Swagger Schemas

Swagger also offers the display of schemas. With a small addition to the nest-cli.json file, NestJS will automatically find and generate them for Swagger.

{
  "collection":"@nestjs/schematics",
  "sourceRoot": ``src",
  "compilerOptions": {
    "plugins": [
      {
        "name": "@nestjs/swagger/plugin",
        "options": {
          "dtoFileNameSuffix": [".dto.ts", ".response.ts", ".model.ts"]
        }
      }
    ]
  }
}

Swagger Schemas List

And if all schemas are properly documented (with decorators as for routes), then Swagger will display it something like this:

Swagger Route Documentation with Scheme

Pipes

Last but not least, I'll briefly turn to the useful pipes. As already mentioned, pipes validate and transform parameters and/or query components.

An illustrative example is the following scenario:
A route takes a MongoDB ObjectID and is supposed to return the record from the database. Mongoose is used. Problem: Mongoose throws an error if the format (too long, too short, etc.) for the ObjectID does not match.

Now the question arises: where should the check take place? Inside the service is bad, because the logic has nothing to do there. Should a function be written so that it can be used multiple times if more routes are added? Where to put it? Utility class into a "shared-module"?

The initial position without checking might look like this:

@Controller('ue')
export class UEController {
  // ...

  @Get('node')
  async getDBNode(@Query('id') objectID: string): Promise<{}> {
    return this.ueService.getNode(objectID);
  }
}

Now we expect the query component id to be a string. Conclusion is that getNode now also expects a string. However, this makes little sense, because as a developer you expect an ObjectID here. With the use of a pipe, the string can be checked and transformed to the format of an ObjectID. In the controller itself there is not even another line.

@Controller('ue')
export class UEController {
  // ...

  @Get('node')
  async getDBNode(
    @Query('id', ParseObjectIdPipe) objectID: Types.ObjectId,
  ): Promise<{}> {
    return this.ueService.getNode(objectID);
  }
}

The pipe ParseObjectIdPipe is swapped out to an extra file.

@Injectable()
export default class ParseObjectIdPipe implements PipeTransform {
  transform(value: any) {
    try {
      return new Types.ObjectId(value);
    } catch (e) {
      throw new BadRequestException('Validation of the id failed');
    }
  }
}

Conclusion

Even with this knowledge, at least a simple REST API can be built. NestJS makes this possible not only through abstraction, but also through its simple and easily comprehensible architecture. At the same time, a Swagger documentation is created, which simplifies the testing of the backend and serves as a point of contact for future (frontend) developers for the interface.

Sources

[1] Twitter - Marko ⚡ Denic