Dev

Intro to Nest.js: The higher-order JavaScript and TypeScript server


Not to be confused with Next.js, Nest.js is a newer and unique approach to JavaScript server technology. It takes a familiar server like Express or Fastify and layers on a number of useful abstractions, which are geared toward empowering and simplifying higher-level application design. Thanks to its distinctive blend of programming paradigms, first-order TypeScript support, and built-in features like dependency injection, Nest.js has steadily grown in popularity over the last few years. 

Nest.js is an interesting contribution to the JavaScript ecosystem, and well worth your attention. It’s a great tool to keep in mind when working with server-side JavaScript and TypeScript.

Overview

In this article, we’ll do a whirlwind tour of Nest.js, with examples including routing, controllers, producers (dependency injection), and authentication with guards. You’ll also get an understanding of the Nest.js module system. 

Our example is an application used for managing a list of pasta recipes. We’ll include a dependency-injected service that manages the actual dataset and a RESTful API we can use to list all recipes or recover a single recipe by ID. We’ll also set up a simple authenticated PUT endpoint for adding new recipes. 

Let’s start by scaffolding a new project. Once have that, we can dive into the examples.

Set up the Nest.js demo

We can use the Nest.js command-line interface to set up a quick application layout, starting with installing Nest globally with: $ npm install -g @nestjs/cli. In addition to the create command, nestjs includes useful features like generate for sharing reusable designs. Installing globally gives us access to that and more.

Now we can create a new application with: $ nest new iw-nest. You can select whichever package manager you want (npm, yarn, or pnpm). For this demo, I’ll use pnpm. The process is the same regardless.

Change into the new  /iw-nest directory and start the dev server with: $ pnpm run start. You can verify the application is running by visiting localhost:3000, where you should see a “Hello, World!” message. This message is coming from the iw-nest/src/app.controller.ts. If you look at that file, you can see it’s using an injected service. Let’s create a new controller (src/recipes.controller.ts) that returns a list of recipes, as shown in Listing 1.

Listing 1. recipe.controller.ts


import { Controller, Get, Inject } from '@nestjs/common';

@Controller('recipes')
export class RecipesController {
  @Get()
  getRecipes() {
    return '[{"name":"Ravioli"}]';
  }
}

Routing with controllers

Listing 1 gives us a look at the basics of routing in Nest.js. You can see we use the @Controller(‘recipes’) annotation to define the class as a controller with the route of /recipes. The getRecipes() method is annotated to handle the GET method with @Get()

For now, this controller simply maps the /recipes GET to a hard-coded response string. Before Nest.js will serve this, we need to register the new controller with the module. Modules are another important concept in Nest, used to help organize your application code. In our case, we need to open /src/app.module.ts and add in the controller, as shown in Listing 2.

Listing 2. Adding the new controller to the module


import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
// Our new controller:
import { RecipesController } from './recipes.controller'; 

@Module({
  imports: [],
  controllers: [AppController,RecipesController],
  providers: [AppService],
})
export class AppModule {}

The dependency injection framework in Nest.js is reminiscent of Spring in the Java ecosystem. Having built-in dependency injection alone makes Nest.js worth considering, even without its other bells and whistles.

We’re going to define a service provider and wire it to our controller. This is a clean way to keep the application organized into layers. You can see our new service class, /src/recipes.service.ts, in Listing 3.

Listing 3. /src/recipes.service.ts


import { Injectable } from '@nestjs/common';

@Injectable()
export class RecipesService {
  private readonly recipes = [
    {
      name: 'Ravioli',
      ingredients: ['pasta', 'cheese', 'tomato sauce'],
    },
    {
      name: 'Lasagna',
      ingredients: ['pasta', 'meat sauce', 'cheese'],
    },
    {
      name: 'Spaghetti',
      ingredients: ['pasta', 'tomato sauce'],
    },
  ];

  getRecipes() {
    return this.recipes;
  }
}

Add the service to the module

To use this service provider, we also need to add it to the app.module.ts file, as shown in Listing 4.

Listing 4. Add the service to the module


@Module({
  imports: [],
  controllers: [AppController,RecipesController],
  providers: [AppService, RecipesService] // add the service 
})

Modules are a good way to organize an application. They can act as a logical grouping mechanism, providing a hierarchical structure where the most fundamental modules are clearly defined and the others depend on them.

Using the service

Now we can use the service in the RecipesController, as shown in Listing 5. If you are new to dependency injection, this might seem like a lot of extra work. But the ability to define and consume classes application-wide, in a standardized way, can be a real boon to your application architecture as the system grows. 

Listing 5. Using the service in the RecipesController


import { Controller, Get, Inject } from '@nestjs/common';
import { RecipesService } from './recipes.service';

@Controller('recipes')
export class RecipesController {
  @Inject()
  private readonly recipesService: RecipesService;
  @Get()
  getRecipes() {
    return this.recipesService.getRecipes();
  }
}

Essentially, we import the RecipesService class, then to get a reference to it, we use the @Inject() annotation on the recipesService member. The injection system will wire this to an instance of the RecipesService class, based on the type. By default, injected services are singletons in Nest, so all client classes will get the reference to the same instance. It’s possible to use other “scopes” for services to fine-tune how they are instantiated. Besides constructor injection, Nest supports property-based injection.

Now, if you run the application and go to localhost:3000/recipes, you’ll see the JSON output from the array of recipes in the service.

Create a POST endpoint

Now, let’s add a new POST endpoint to allow users to add recipes. We’ll secure it with the simplest possible authentication using what’s called a guard in Nest.js. A guard sits in front of the controllers and determines how the requests are routed. You can see our simple guard in Listing 6. Right now, it just checks whether there is an authentication header in the request.

Listing 6. A simple AuthGuard


import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    // Simple authentication logic
    const request = context.switchToHttp().getRequest();
    const authHeader = request.headers.authorization;
    return authHeader === 'Bearer secret-token';
  }
}

Next, register the guard with the module, as shown in Listing 7.

Listing 7. Register the guard with app.module.ts


// ...
import { AuthGuard } from './auth.guard';

@Module({
  imports: [],
  controllers: [AppController, RecipesController],
  providers: [AppService, RecipesService, AuthGuard], 
})
export class AppModule {}

Now we can use the new guard service to protect our POST endpoint, shown in Listing 8. Note the new imports.

Listing 8. Protecting the POST endpoint


import { Controller, Get, Inject, Post, Body, UseGuards } from '@nestjs/common';
import { RecipesService } from './recipes.service';
import { AuthGuard } from "./auth.guard";

@Controller('recipes')
export class RecipesController {
  @Inject()
  private readonly recipesService: RecipesService;
  @Get()
  getRecipes() {
    return this.recipesService.getRecipes();
  }
  @Post()
  @UseGuards(AuthGuard)
  addRecipe(@Body() recipe: any) {
    return this.recipesService.addRecipe(recipe);
  }
}

Notice that the @UserGuards annotation was used to apply the new guard to the addRecipe() method, which is also specified as a POST endpoint with the @Post annotation. Nest will take care of instantiating and applying the guard to the endpoint for us.

We’ve also added an addRecipe() method to the service, which is very simple, as shown in Listing 9.

Listing 9. /src/recipes.service.ts addRecipe method


addRecipe(recipe: any) {
    this.recipes.push(recipe);
    return recipe;
  }
  

Now we can test out the authentication and endpoint with a couple of CURL requests, as in Listing 10.

Listing 10. Testing the new endpoint and authguard


$ curl -X POST -H "Authorization: Bearer secret-token" -H "Content-Type: application/json" -d '{"name": "Carbonara", "ingredients": ["pasta", "eggs", "bacon"]}' http://localhost:3000/recipes

{"name":"Carbonara","ingredients":["pasta","eggs","bacon"]}

$ curl -X POST -H "Content-Type: application/json" -d '{"name": "Carbonara", "ingredients": ["pasta", "eggs", "bacon"]}' http://localhost:3000/recipes

{"message":"Forbidden resource","error":"Forbidden","statusCode":403}

You can see the authorization is working, as only a request holding the Bearer header is allowed through.

Using Nest with TypeScript

So far, we have used just a JavaScript object. In the TypeScript world, it would be common to create a Recipe model object and use it as a value object to shuttle around information. For example, we could create the Recipe class (Listing 11) and use it in the addRecipe method (Listing 12).

Listing 11. Recipe model


export class Recipe {
  constructor(public name: string, public ingredients: string[]) {}

  getName(): string {
    return this.name;
  }

  getIngredients(): string[] {
    return this.ingredients;
  }
}

Finally, you can make the addRecipe() POST method strongly typed and Next will automatically populate the model object for us:

Listing 12. Strongly typed POST method


import { Injectable } from '@nestjs/common';
import { Recipe } from './recipe.model';

@Injectable()
export class RecipesService {
  private readonly recipes: Recipe[] = [
    new Recipe('Ravioli', ['pasta', 'cheese', 'tomato sauce']),
    new Recipe('Lasagna', ['pasta', 'meat sauce', 'cheese']),
    new Recipe('Spaghetti', ['pasta', 'tomato sauce']),
  ];

  getRecipes(): Recipe[] {
    return this.recipes;
  }

  addRecipe(recipe: Recipe): Recipe {
    this.recipes.push(recipe);
    return recipe;
  }
}

You can then make the addRecipe() POST method strongly typed and Nest will automatically populate the model object for us, as shown in Listing 13.

Listing 13. Strongly typed POST method


// ...
import { Recipe } from './recipe.model';

@Controller('recipes')
export class RecipesController {
  // ...
  @Post()
  @UseGuards(AuthGuard)
  addRecipe(@Body() recipe: Recipe) {
    const newRecipe = new Recipe(recipe.name, recipe.ingredients);
    return this.recipesService.addRecipe(newRecipe);
  }
}

Conclusion

The choice between the flexibility of JavaScript’s duck typing and TypeScript’s strong typing is really down to what you, the team, or the organization decide to use. JavaScript gives you speed of development and flexibility, whereas TypeScript gives you structure and more tooling support.

It’s also worth noting that Nest.js embraces reactive programming, and you can return promises from methods and endpoints. Even more, you can return an RxJS Observable. This gives you powerful options for wiring applications together with asynchronous data streams.

Although we’ve only scratched the surface of what it can do, it’s clear that Nest.js is a well-thought-out and capable platform for building Node servers. It delivers on the promise of a higher-level layer on top of Express for improved architecture and design support. If you are looking to build server-side JavaScript and especially TypeScript applications, Nest is a great option.

Copyright © 2023 IDG Communications, Inc.



READ SOURCE

This website uses cookies. By continuing to use this site, you accept our use of cookies.