DEV Community

Cover image for Creating a REST API with NestJS
Wallace Daniel
Wallace Daniel

Posted on • Updated on • Originally published at thefullstack.engineer

Creating a REST API with NestJS

Welcome back! In part 2 of this series, I'll cover some basics of NestJS, establish the purpose of this application, demonstrate how shared data structures can be used, and implement a proper REST API.

Other posts in this series:

If you want to skip ahead to the code, you can checkout out the repository: wgd3/full-stack-todo@part-02

If you'd like access to these articles as soon as they're available, or would like to be notified when new content has been posted, please subscribe to my newsletter: The Full Stack Engineer Newsletter

What's This App Going To Do?

Our full stack application is going to... serve a to-do list! Not the most exciting thing in the world, I know, but it's the perfect way to demonstrate all layers of a full-stack application. To that end, our first step is going to be establishing data structures that both the client and server apps will use.

Sharing Is Caring

Nx has some excellent documentation on their philosophy behind directory structure and shared folders. Following their lead, let's create our first library:

     ~/git/full-stack-todo | main
    > npx nx generate @nrwl/workspace:library domain \
    --directory=shared \
    --importPath=@fst/shared/domain \
    --skipBabelrc \
    --standaloneConfig \
    --tags=scope:shared,type:domain
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ Code blocks are inherently easier to read, parse, copy/paste. To be honest though, I usually use the Nx Console plugin for VSCode to have a GUI and ensure I don't miss any flags!

Explaining what just happened

This command just did a lot of work for us. It automatically:

  • Created the libs/shared directory
  • Instantiated a new library under libs/shared/domain with all the config files necessary for an individual library
  • Updated the tsconfig.base.json file with a new import path (this allows us to not use relative imports in other libraries)
  • Added tags to the project.json for the library. More on tags soon.

With a shared library in place, we can create our first interface:

// libs/shared/domain/src/lib/models/todo.interface.ts`

    export interface ITodo {
        id: string;
        title: string;
        description: string;
        completed: boolean;
    }
Enter fullscreen mode Exit fullscreen mode

There are a couple of conventions I follow in my projects:

  • One file per interface
  • Files that export an interface should use the naming scheme <name>.interface.ts. This is extremely useful when tools accept a blob pattern to lookup files, and you can specify exactly what files you want. And it's easier to read in my opinion.
  • Interface names should be prefaced with an I for easier identification throughout the codebase

While updating this library, let's remove two auto-generated files that aren't needed:

     ~/git/full-stack-todo | main
    > rm libs/shared/domain/src/lib/shared-domain*
    remove libs/shared/domain/src/lib/shared-domain.spec.ts? y
    remove libs/shared/domain/src/lib/shared-domain.ts? y
Enter fullscreen mode Exit fullscreen mode

And lastly, update the index.ts file:

// libs/shared/domain/src/index.ts

    // remove this line
    export * from './lib/shared-domain';

    // add this line
    export * from './lib/models/todo.interface';
Enter fullscreen mode Exit fullscreen mode

Our to-do interface is now available anywhere in the application!

Building Blocks

I've mentioned before that NestJS was built similarly to Angular in terms of data flow and structure. If you're new to NestJS, let's review some common terms before moving on to the CRUD REST API.

  • Controller Similar to a component in the Angular world, controllers are the "user-facing" part of the framework. This is where API routes are established
  • Service Very similar to Angular services, this is typically where data lookup/manipulation occurs. Controllers should delegate business logic to services as much as possible
  • Pipe Like Angular, pipes are meant for validating or manipulating data during the request. In this article we'll show a "global" pipe that's used for validating JSON payloads
  • Module Modules act as you might expect - they are pluggable groupings of controllers, services, and other providers. We'll use a feature module to manage our first controller and service, and the main AppModule will simply import the feature module

CRUD REST API

Time to write some actual NestJS-specific code. We'll use the Nx generator to create a new feature library:

    ~/git/full-stack-todo | main
    > npx nx generate @nrwl/nest:library feature-todo \
    --directory=server \
    --controller \
    --importPath=@fst/server/feature-todo \
    --service \
    --strict \
    --tags=scope:server,type:feature
Enter fullscreen mode Exit fullscreen mode

Explaining The Command

Another great generator from Nx, @nrwl/nest:library sets up a fresh library with some needed components:

  • ServerFeatureTodoController will host our API routes
  • ServerFeatureTodoService will handle our data
  • ServerFeatureTodoModule will be imported by AppModule soon

The command also used a new tag: type:feature with a small scope scope:server. I promise we'll get to tags before the end of this post.

Our first controller:

    // libs/server/feature-todo/src/lib/server-feature-todo.controller.ts
    @Controller('server-feature-todo')
    export class ServerFeatureTodoController {
      constructor(private serverFeatureTodoService: ServerFeatureTodoService) {}
    }
Enter fullscreen mode Exit fullscreen mode

Before we add a route, there needs to be some form of a data store. A later article will address the TypeORM integration, so for now let's use a BehaviorSubject:

    // libs/server/feature-todo/src/lib/server-feature-todo.service.ts

    import { Injectable } from '@nestjs/common';
    // importing our interface using the custom import path!
    import { ITodo } from '@fst/shared/domain';
    import { BehaviorSubject } from 'rxjs';

    @Injectable()
    export class ServerFeatureTodoService {
        private todos$$ = new BehaviorSubject<ITodo[]>([]);

        getAll(): ITodo[] {
            return this.todos$$.value;
        }

        getOne(id: string): ITodo {
            const todo = this.todos$$.value.find(td => td.id === id);
            return todo;
        }
    }
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ Sometimes VSCode likes to take it's sweet time recognizing changes to tsconfig.base.json - in particular when new import paths are created. I manually typed out the import above, because the autocomplete import pulled from libs/shared/domain and that resulted in red squigglies. Once I corrected the import path, I used the Restart TS Command to refresh the available paths. On a Mac, Shift+Space+P launches the command prompt, and you can start typing to find the command.

I'm using a BehaviorSubject to store an empty array initially, and made it private so that no other class can directly access the BehaviorSubject - we're forced to add methods to the service for altering data. With a "data store" in place (yes, it's ephemeral, it'll reset every time the server app launches), we can start adding routes to the controller.

// libs/server/feature-todo/src/lib/server-feature-todo.controller.ts

    @Controller('server-feature-todo')
    export class ServerFeatureTodoController {
      constructor(private serverFeatureTodoService: ServerFeatureTodoService) {}

      @Get('')
      getAll(): ITodo[] {
        return this.serverFeatureTodoService.getAll();
      }

      @Get(':id')
      getOne(@Param('id') id: string): ITodo {
        return this.serverFeatureTodoService.getOne(id);
      }
    }
Enter fullscreen mode Exit fullscreen mode

Our first routes! NestJS uses decorators to signal which methods should be used as API routes, and what type of HTTP request is performed on that route. We're specifying 2 GET routes, one for all of the to-dos, and one for a particular to-do.

I'm sure you're wondering why I left the getOne() method in the service, even though the IDE is complaining. The find() method isn't guaranteed to return a to-do - if no matching id is found, then it will return undefined. Let's fix this:

// libs/server/feature-todo/src/lib/server-feature-todo.service.ts

    @Injectable()
    export class ServerFeatureTodoService {
        private todos$$ = new BehaviorSubject<ITodo[]>([]);

        getAll(): ITodo[] {
            return this.todos$$.value;
        }

        getOne(id: string): ITodo {
            const todo = this.todos$$.value.find(td => td.id === id);
            if (!todo) {
                throw new NotFoundException(`Todo could not be found!`)
            }
            return todo;
        }
    }
Enter fullscreen mode Exit fullscreen mode

If todo is undefined here, we use NestJS' Built-in HTTP exceptions to return a 404 to the user.

One last step before we're able to use the API: adding our module to the AppModule:

// apps/server/src/app/app.module.ts

    import { ServerFeatureTodoModule } from '@fst/server/feature-todo';
    import { Module } from '@nestjs/common';

    import { AppController } from './app.controller';
    import { AppService } from './app.service';

    @Module({
      imports: [ServerFeatureTodoModule],
      controllers: [AppController],
      providers: [AppService],
    })
    export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

And with that, we can now see our feature module and it's API routes in Nest's logger:

    ~/git/full-stack-todo | main
    > nx serve server

    [Nest] 6607  - 02/10/2023, 2:03:54 PM     LOG [NestFactory] Starting Nest application...
    [Nest] 6607  - 02/10/2023, 2:03:54 PM     LOG [InstanceLoader] AppModule dependencies initialized +15ms
    [Nest] 6607  - 02/10/2023, 2:03:54 PM     LOG [InstanceLoader] ServerFeatureTodoModule dependencies initialized +0ms
    [Nest] 6607  - 02/10/2023, 2:03:54 PM     LOG [RoutesResolver] AppController {/api}: +8ms
    [Nest] 6607  - 02/10/2023, 2:03:54 PM     LOG [RouterExplorer] Mapped {/api, GET} route +3ms
    [Nest] 6607  - 02/10/2023, 2:03:54 PM     LOG [RoutesResolver] ServerFeatureTodoController {/api/server-feature-todo}: +0ms
    [Nest] 6607  - 02/10/2023, 2:03:54 PM     LOG [RouterExplorer] Mapped {/api/server-feature-todo, GET} route +1ms
    [Nest] 6607  - 02/10/2023, 2:03:54 PM     LOG [RouterExplorer] Mapped {/api/server-feature-todo/:id, GET} route +1ms
    [Nest] 6607  - 02/10/2023, 2:03:54 PM     LOG [NestApplication] Nest application successfully started +6ms
    [Nest] 6607  - 02/10/2023, 2:03:54 PM     LOG πŸš€ Application is running on: http://localhost:3333/api
Enter fullscreen mode Exit fullscreen mode

You know thats rightYou know that's right

Interacting With The API

Great, the routes are there - now how do we use them? There are plenty of tools out there such as the well-known Postman, but I'm going to stick to the command line for now. We'll add Swagger docs (a UI for your API that's automatically generated) in a later post.

I used Homebrew to install httpie, and can now make GET requests against the routes:

     ~/git/full-stack-todo | main
    > http localhost:3333/api/server-feature-todo

    HTTP/1.1 200 OK

    []
Enter fullscreen mode Exit fullscreen mode

It returned the empty array!
Empty arrays are nothing to get excited about, so I'm going to hard code a to-do item in my service:

// libs/server/feature-todo/src/lib/server-feature-todo.service.ts

        private todos$$ = new BehaviorSubject<ITodo[]>([
            {
                id: 'something-something-dark-side',
                title: 'Add a route to create todo items!',
                description: 'Yes, this is foreshadowing a POST route introduction',
                completed: false
            }
        ]);
Enter fullscreen mode Exit fullscreen mode

Now I can make that API call again and see my hard-coded item:

     ~/git/full-stack-todo | main 
    > http localhost:3333/api/server-feature-todo
    HTTP/1.1 200 OK

    [
        {
            "completed": false,
            "description": "Yes, this is foreshadowing a POST route introduction",
            "id": "something-something-dark-side",
            "title": "Add a route to create todo items!"
        }
    ]
Enter fullscreen mode Exit fullscreen mode

Checking Off The To-do List

We have a way to retrieve data, but we also need to be able to create, update, and delete to-do items. Pivoting for a moment, I'd like to review the various HTTP request methods and how they're supposed to be used with a REST API:

  • GET Retrieves a representation of the resource at the specified URI
  • POST creates a new resource at the specified URI
  • PUT replaces an existing resource at the specified URI, or creates it if it doesn't exist
  • PATCH updates part or all properties of the resource at the specified URI
  • DELETE deletes the resource at the specified URI

Since I've added a to-do for a POST route, that's what I'll add next.

// libs/server/feature-todo/src/lib/server-feature-todo.service.ts

        create(todo: ITodo): ITodo {
            const current = this.todos$$.value;
            this.todos$$.next([...current, todo]);
            return todo;
        }
Enter fullscreen mode Exit fullscreen mode
// libs/server/feature-todo/src/lib/server-feature-todo.controller.ts

      @Post('')
      create(@Body() data: ITodo): ITodo {
        return this.serverFeatureTodoService.create(data);
      }
Enter fullscreen mode Exit fullscreen mode

There's a small problem with this code though - once a database is involved, the id property of a to-do will be created automatically (provided you're using auto-incrementing primary keys). We don't want users manually specifying the id, or creating a to-do item marked complete: true. And it has to have a title! All of these issues can be addressed by creating a Data Transfer Object with validation.

Data Transfer Objects and Payload Validation

Since our POST route is using an interface for the @Body decorator, there's no real enforcement of the data structure - this just tells the compiler and IDE that data should look that way in the request. Switching to a class-based DTO pattern (and telling NestJS to validate payloads) will avoid this issue. Before we create the DTO, install a couple of helper packages that we'll use for validation:

    ~/git/full-stack-todo | main
    > npm i -S class-validator class-transformer
Enter fullscreen mode Exit fullscreen mode

Now let's create a DTO for the ITodo objects:

// libs/server/feature-todo/src/lib/dtos/todo.dto.ts

    import {
      IsNotEmpty,
      IsString,
    } from 'class-validator';
    import { ITodo } from '@fst/shared/domain';

    /**
     * Use the `Pick` utility type to extract only the properties we want for
     * new to-do items
     */
    export class CreateTodoDto implements Pick<ITodo, 'title' | 'description'> {
      @IsString()
      @IsNotEmpty()
      title!: string;

      @IsString()
      @IsNotEmpty()
      description!: string;
    }
Enter fullscreen mode Exit fullscreen mode

Our POST route needs to be updated to use this class:

// libs/server/feature-todo/src/lib/server-feature-todo.controller.ts

      @Post('')
      create(@Body() data: CreateTodoDto): ITodo {
        return this.serverFeatureTodoService.create(data);
      }
Enter fullscreen mode Exit fullscreen mode

And the IDE is now complaining because create(data) is not passing in the expected ITodo - so the service should be updated as well:

// libs/server/feature-todo/src/lib/server-feature-todo.service.ts

      /**
       * Update the arg signature to match the DTO, but keep the
       * return signature - we still want to respond with the complete
       * object
       */
      create(todo: Pick<ITodo, 'title' | 'description'>): ITodo {
        const current = this.todos$$.value;
        // Use the incoming data, a randomized ID, and a default value of `false` to create the new to-do
        const newTodo: ITodo = {
          ...todo,
          id: `todo-${Math.floor(Math.random() * 10000)}`,
          completed: false,
        };
        this.todos$$.next([...current, newTodo]);
        return newTodo;
      }
Enter fullscreen mode Exit fullscreen mode

One last file to update before we can test the validation:

// apps/server/src/main.ts

    async function bootstrap() {
      const app = await NestFactory.create(AppModule);
      const globalPrefix = 'api';
      app.setGlobalPrefix(globalPrefix);

      /** add this line!! */
      app.useGlobalPipes(new ValidationPipe({ transform: true }))

      const port = process.env.PORT || 3333;
      await app.listen(port);
      Logger.log(
        `πŸš€ Application is running on: http://localhost:${port}/${globalPrefix}`
      );
    }
Enter fullscreen mode Exit fullscreen mode

And the result:

    ~/git/full-stack-todo | main
    > http localhost:3333/api/server-feature-todo title=foo description=bar

    HTTP/1.1 201 Created

    {
        "completed": false,
        "description": "bar",
        "id": "todo-3827",
        "title": "foo"
    }

    # checking that the valdiation works as well

     ~/git/full-stack-todo | main
    > http localhost:3333/api/server-feature-todo title="this will fail"

    HTTP/1.1 400 Bad Request

    {
        "error": "Bad Request",
        "message": [
            "description should not be empty",
            "description must be a string"
        ],
        "statusCode": 400
    }
Enter fullscreen mode Exit fullscreen mode

Success!

Library Types and Tags

Now to address the "library types" I've referred to a few times. Nx has a really good starting point for this so I won't type it all out here. It is important that tags are being properly used from the start, however, as it can be a hassle to go back through dozens of libraries to make sure they're all in check. Here's the current state of our apps and libs:

     ~/git/full-stack-todo | main
    > grep tags {libs,apps}/**/project.json
    libs/server/feature-todo/project.json:  "tags": ["scope:server", "type:feature"]
    libs/shared/domain/project.json:  "tags": ["scope:shared", "type:domain"]
    apps/client-e2e/project.json:  "tags": [],
    apps/client/project.json:  "tags": ["type:app", "scope:client"]
    apps/server/project.json:  "tags": ["scope:server","type:app"]
Enter fullscreen mode Exit fullscreen mode

The .eslintrc.json file does not have any boundaries defined for these tags, so let's update that:

    {
          "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
          "rules": {
            "@nrwl/nx/enforce-module-boundaries": [
              "error",
              {
                "enforceBuildableLibDependency": true,
                "allow": [],
                "depConstraints": [
                  {
                    "sourceTag": "*",
                    "onlyDependOnLibsWithTags": ["*"]
                  },
                  // add these 2 entries below!
                  {
                    "sourceTag": "scope:server",
                    "onlyDependOnLibsWithTags": ["scope:server", "scope:shared"]
                  },
                  {
                    "sourceTag": "scope:client",
                    "onlyDependOnLibsWithTags": ["scope:client", "scope:shared"]
                  }
                ]
              }
            ]
          }
        },
Enter fullscreen mode Exit fullscreen mode

This does not address the type tags that we've added to libraries, but for now we'll make sure that server and client don't get any code mixed up. And at the moment, our linting passes!

     ~/git/full-stack-todo | main
    > nx run-many --target=lint --all

        βœ”  nx run shared-domain:lint (3s)
        βœ”  nx run server-feature-todo:lint (3s)
        βœ”  nx run client:lint (3s)
        βœ”  nx run server:lint (2s)
        βœ”  nx run client-e2e:lint (2s)
        βœ”  nx run server-e2e:lint (2s)

     β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”

     >  NX   Successfully ran target lint for 6 projects (5s)
Enter fullscreen mode Exit fullscreen mode

Final Notes

We now officially have a REST API for our to-do items! In the tagged version of the repository for this part, you'll notice that I've fleshed out the remaining CRUD routes for to-do items, updated the service, and added DTOs as needed. Β That code can be found here: wgd3/full-stack-todo@part-02

In the next post, we'll add Swagger documentation to our routes, improve the DTOs and shared interfaces, and add tests; all done in an effort to have a bulletproof API before the Angular client starts using it. Stay tuned and thanks for reading!

     ~/git/full-stack-todo | main
    > http PATCH localhost:3333/api/server-feature-todo/something-something-dark-side completed:=true

    HTTP/1.1 200 OK

    {
        "completed": true,
        "description": "Yes, this is foreshadowing a POST route introduction",
        "id": "something-something-dark-side",
        "title": "Add a route to create todo items!"
    }
Enter fullscreen mode Exit fullscreen mode

Feels like an appropriate way to sign off for this post!

Top comments (0)