Skip to the content.

Go Back to Home

Building RESTful APIs with NestJS

In this post, I will demonstrate how to kickstart a simple RESTful APIs with NestJS from a newbie’s viewpoint.

What is NestJS?

As described in the Nestjs website, Nestjs is a progressive Node.js framework for building efficient, reliable and scalable server-side applications.

Nestjs combines the best programming practice and the cutting-edge techniques from the NodeJS communities.

If you are new to Nestjs like me but has some experience of Angular , TypeDI or Spring WebMVC, bootstraping a Nestjs project is really a piece of cake.

Generating a NestJS project

Make sure you have installed the latest Nodejs.

npm i -g @nestjs/cli

When it is finished, there is a nest command available in the Path. The usage of nest is similar with ng (Angular CLI), type nest --help in the terminal to list help for all commands.

❯ nest --help
Usage: nest <command> [options]

Options:
  -v, --version                                   Output the current version.
  -h, --help                                      Output usage information.

Commands:
  new|n [options] [name]                          Generate Nest application.
  build [options] [app]                           Build Nest application.
  start [options] [app]                           Run Nest application.
  info|i                                          Display Nest project details.
  update|u [options]                              Update Nest dependencies.
  add [options] <library>                         Adds support for an external library to your project.
  generate|g [options] <schematic> [name] [path]  Generate a Nest element.
    Available schematics:
      ┌───────────────┬─────────────┐
      │ name          │ alias       │
      │ application   │ application │
      │ class         │ cl          │
      │ configuration │ config      │
      │ controller    │ co          │
      │ decorator     │ d           │
      │ filter        │ f           │
      │ gateway       │ ga          │
      │ guard         │ gu          │
      │ interceptor   │ in          │
      │ interface     │ interface   │
      │ middleware    │ mi          │
      │ module        │ mo          │
      │ pipe          │ pi          │
      │ provider      │ pr          │
      │ resolver      │ r           │
      │ service       │ s           │
      │ library       │ lib         │
      │ sub-app       │ app         │
      └───────────────┴─────────────┘

Now Generate a NestJS API via:

nest new nestjs-sample

Open it in your favorite IDEs, such as Intellij WebStorm or VSCode.

Exploring the project files

Expand the project root, you will see the following like tree nodes.

.
├── LICENSE
├── nest-cli.json
├── package.json
├── package-lock.json
├── README.md
├── src
│   ├── app.controller.spec.ts
│   ├── app.controller.ts
│   ├── app.module.ts
│   ├── app.service.ts
│   └── main.ts
├── test
│   ├── app.e2e-spec.ts
│   └── jest-e2e.json
├── tsconfig.build.json
└── tsconfig.json

The default structure of this project is very similar with the one generated by Angular CLI.

## Defining the APIs

I will reuse the concept I’ve used in the former examples - the blogging posts.

In the next steps, we will create:

Creating PostService

Following the Nestjs coding style, first of all, let’s generate a module named post:

nest g mo post

PosModule is imported into the top-level AppModule. Check the content of file src/app/app.module.ts:

//... other imports
import { PostModule } from './post/post.module';

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

Then generate a Post service and interface respectively.

nest g s post
nest g interface post

The PostService is added to PostModule automatically when it is generated.

//...other imports
import { PostService } from './post.service';

@Module({
  providers: [PostService]
})
export class PostModule {}

After it is done there are 4 files created in the src/app/post folder, including a spec file for testing PostService.

post.interface.ts
post.module.ts
post.service.spec.ts
post.service.ts

Firstly, let’s open post.interface.ts to model the Post entity.

export interface Post {
 id?: number;
 title:string;
 content: string;
 createdAt?: Date,
 updatedAt?: Date
}

In the above interface, add some fields as you see, here we make the id, createdAt, updatedAt optional now.

Let’s create a method in PostService to fetch all posts.

@Injectable()
export class PostService {
  private posts: Post[] = [
    {
      id: 1,
      title: 'Generate a NestJS API',
      content: 'content',
      createdAt: new Date(),
    },
    {
      id: 2,
      title: 'Create RESTful CRUD Web Service',
      content: 'content',
      createdAt: new Date(),
    },
    {
      id: 3,
      title: 'Connect to Mongodb Database',
      content: 'content',
      createdAt: new Date(),
    },
  ];

  findAll(): Observable<Post> {
    return from(this.posts);
  }
}    

In the codes, we used an array posts as the background data storage. The findAll method returns a Observerable which is created from the dummy posts we have declared.

Add a test for this method. Open post.service.spec.ts, add a new test case.


describe('PostService', () => {
  let service: PostService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [PostService],
    }).compile();

    service = module.get<PostService>(PostService);
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  it('getAllPosts should return 3 posts', done => {
    service
      .findAll()
      .pipe(take(3), toArray())
      .subscribe({
        next: data => expect(data.length).toBe(3),
        error: error => console.log(error),
        complete: done(),
      });
  });
}

Similar with Angular, in the beforeEach hook , it prepares a TestingModule to assemble the required resources in the test.

Like the annotations (@BeforeEach, etc.) provided JUnit 5 in Java world, there are some similar hooks ready for preparing the test and doing clean work in a Jest test, such as beforeAll, afterAll, beforeEache, afterEach , etc.

In the newly added it('getAllPosts should return 3 posts', done => {} , it uses a done handler to mark the an asynchronous task is done. It is very helpful to test Promise or Observerable invocation in a test.

The pipe accepts a series of Rxjs operators for some processor working, such as filtering, transforming, collecting, etc. check the Rxjs official docs.

The subscribe accept a subscriber to handle the data stream, see the Subscriber API for more details.

Run npm run test command in the root folder to run all tests.

❯ npm run test

> nestjs-sample@0.0.1 test D:\souyanglabs\nestjs-sample
> jest

...
Time:        4.563 s, estimated 11 s
Ran all test suites.

Awesome, it works.

If you want to track the changes of test codes and rerun the test cases automatically, use npm run test:watch instead.

Let’s modify the findAll slightly, make it accept a keyword to query the posts.

@Injectable()
export class PostService {
  //...
  findAll(keyword?: string): Observable<Post> {
    if (keyword) {
      return from(this.posts.filter(post => post.title.indexOf(keyword) >= 0));
    }

    return from(this.posts);
  }
}

Add another test to verify filtering the posts by keyword.

it('getAllPosts with keyword should return 1 post', done => {
    service
        .findAll('Generate')
        .pipe(take(3), toArray())
        .subscribe({
        next: data => expect(data.length).toBe(1),
        error: error => console.log(error),
        complete: done(),
    });
});

When using a keyword Generate, only one item will be included in the data stream. Run the test again, it will pass.

Let’s move the next one. Create a findById method in PostService class. When a post is found return the found post else return a Rxjs EMPTY.

findById(id: number): Observable<Post> {
    const found = this.posts.find(post => post.id === id);
    if (found) {
      return of(found);
    }
    return EMPTY;
}

And add a test for the case when a post is found.

it('getPostById with existing id should return 1 post', done => {
    service.findById(1).subscribe({
        next: data => {
            expect(data.id).toBe(1);
            expect(data.title).toEqual('Generate a NestJS API');
        },
        error: error => console.log(error),
        complete: done(),
    });
});

When the findById return a EMPTY, I tried to use the same approach to verify the result in the next handler, but failed, the next is never called, I started a topic on stackoverflow, and got two solutions for handling the EMPTY stream.

The first solution is utilizing the testing facility from Rxjs. Set up a TestScheduler for test, and use expectObservable to assert the data stream in marble diagrams.

beforeEach(() => {
    testScheduler = new TestScheduler((actual, expected) => {
        // asserting the two objects are equal
        expect(actual).toEqual(expected);
    });
});

// This test will actually run *synchronously*
it('test complete for empty()', () => {
    testScheduler.run(helpers => {
        const { cold, expectObservable, expectSubscriptions } = helpers;
        expectObservable(service.findById(10001)).toBe('|');
    });
});

The second test is using a signal myself to make sure it is completed.

it('getPostById with none existing id should return empty', done => {
    let called = false;
    service.findById(10001).subscribe({
        next: data => {
            console.log(data);
            called = true;
        },
        error: error => {
            console.log(error);
            called = true;
        },
        complete: () => {
            expect(called).toBeFalsy();
            done();
        },
    });

Another workaround is converting the EMPTY stream to an array and asserting it is an empty array in the next handler.

it('getPostById with none existing id should return empty', done => {
    service
        .findById(10001)
        .pipe(toArray())
        .subscribe({
        next: data => expect(data.length).toBe(0),
        error: error => console.log(error),
        complete: done(),
    });
});

Ok, we have resolved the emtpy stream testing issue. Go ahead.

Next, let’s create a method to save a new Post into the data storage. Since we are using an array for the dummy storage, it is simple when saving a new Post, just append it into the end of the existing posts:

save(data: Post): Observable<Post> {
    const post = { ...data, id: this.posts.length + 1, createdAt: new Date() };
    this.posts = [...this.posts, post];
    return from(this.posts);
}

Here, we return the new array as result. In a real world application, most of case it is better to return the new persisted object or the id of the new post.

Create a test for saving a new post, verify if the length of the array is increased and if the createdAt is set in the new post.

it('save should increase the length of posts', done => {
    service
        .save({
            id: 4,
            title: 'NestJS Tutorial Title',
            content: 'NestJS Tutorial Content',
        })
        .pipe(toArray())
        .subscribe({
            next: data => {
                expect(data.length).toBe(4);
                expect(data[3].createdAt).toBeTruthy();
            },
            error: error => console.log(error),
            complete: done(),
        });
});

Execute npm run test to make make sure it works.

Similarly, create a update method in PostService class to update the existing post.

update(id: number, data: Post): Observable<Post> {
    this.posts = this.posts.map(post => {
        if (id === post.id) {
            post.title = data.title;
            post.content = data.content;
            post.updatedAt = new Date();
        }
        return post;
    });
    return from(this.posts);
}

Create a test for update method. In the test, we updated the first item in the posts data store, and update it, and finally verify the changes.

it('update should change the content of post', done => {
    service
        .update(1, {
            id: 1,
            title: 'NestJS Tutorial Title',
            content: 'NestJS Tutorial Content',
            createdAt: new Date(),
        })
        .pipe(take(4), toArray())
        .subscribe({
        next: data => {
            expect(data.length).toBe(3);
            expect(data[0].title).toBe('NestJS Tutorial Title');
            expect(data[0].content).toBe('NestJS Tutorial Content');
            expect(data[0].updatedAt).not.toBeNull();
        },
        error: error => console.log(error),
        complete: done(),
    });
});

Create a method to delete post by id.

deleteById(id: number): Observable<boolean> {
    const idx: number = this.posts.findIndex(post => post.id === id);
    if (idx >= 0) {
        this.posts = [
            ...this.posts.slice(0, idx),
            ...this.posts.slice(idx + 1),
        ];
        return of(true);
    }
    return of(false);
}

Create a test case in post.service.test to verify the functionality of the deleteById method.

it('deleteById with existing id should return true', done => {
    service.deleteById(1).subscribe({
        next: data => expect(data).toBeTruthy,
        error: error => console.log(error),
        complete: done(),
    });
});

it('deleteById with none existing id should return false', done => {
    service.deleteById(10001).subscribe({
        next: data => expect(data).toBeFalsy,
        error: error => console.log(error),
        complete: done(),
    });
});

OK, all methods used for CRUD functionalities are ready, let’s move to the controller.

Creating PostController

Like Spring WebMVC, in the NestJS world, the controller is responsible for handling incoming requests from the client, and sending back the handled result to the client.

Generate a controller using nest command:

nest g co post

It will add two files into the src/app/post folder.

post.controller.spec.ts
post.controller.ts

And PostController is registered in PostModule automatcially when it is generated.

//...other imports
import { PostController } from './post.controller';

@Module({
  controllers: [PostController],
  providers: [PostService]
})
export class PostModule {}

Open the post.controller.ts file and enrich the PostController class as we planned.

@Controller('posts')
export class PostController {
  constructor(private postService: PostService) {}

  @Get('')
  getAllPosts(@Query('q') keyword?: string): Observable<BlogPost[]> {
    return this.postService.findAll(keyword).pipe(take(10), toArray());
  }

  @Get(':id')
  getPostById(@Param('id', ParseIntPipe) id: number): Observable<BlogPost> {
    return this.postService.findById(id);
  }

  @Post('')
  createPost(@Body() post: BlogPost):Observable<BlogPost[]> {
    return this.postService.save(post).pipe(toArray());
  }

  @Put(':id')
  updatePost(@Param('id', ParseIntPipe) id: number, @Body() post: BlogPost): Observable<BlogPost[]> {
    return this.postService.update(id, post).pipe(toArray());
  }

  @Delete(':id')
  deletePostById(@Param('id', ParseIntPipe) id: number): Observable<boolean> {
    return this.postService.deleteById(id);
  }
}

In the above codes:

In the Typescript language, we call the @Controller syntax as Decorator. For those who are familiar with Spring WebMVC, using the word annotation is easier to understand.

Now we add tests for PostController.

Open post.controller.spec.ts , add the following tests.

describe('Post Controller', () => {
  let controller: PostController;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [PostService],
      controllers: [PostController],
    }).compile();

    controller = module.get<PostController>(PostController);
  });

  it('should be defined', () => {
    expect(controller).toBeDefined();
  });

  it('GET on /posts should return all posts', async () => {
    const posts = await controller.getAllPosts().toPromise();
    expect(posts.length).toBe(3);
  });

  it('GET on /posts/1 should return one post ', done => {
    controller.getPostById(1).subscribe(data => {
      expect(data.id).toEqual(1);
      done();
    });
  });

  it('POST on /posts should return all posts', async () => {
    const post:Post = {id:4, title:'NestJS Tutorial Title', content:'NestJS Tutorial Content'};
    const posts = await controller.createPost(post).toPromise();
    expect(posts.length).toBe(4);
  });

  it('PUT on /posts/1 should return all posts', done => {
    const post:Post = {id:4, title:'NestJS Tutorial Title', content:'NestJS Tutorial Content'};
    controller.updatePost(1, post).subscribe(data => {
      expect(data.length).toBe(3);
      expect(data[0].title).toEqual('NestJS Tutorial Title');
      expect(data[0].content).toEqual('NestJS Tutorial Content');
      expect(data[0].updatedAt).toBeTruthy();
      done();
    });
  });

  it('DELETE on /posts/1 should return true', done => {
    controller.deletePostById(1).subscribe(data => {
      expect(data).toBeTruthy();
      done();
    });
  });

  it('DELETE on /posts/1001 should return false', done => {
    controller.deletePostById(1001).subscribe(data => {
      expect(data).toBeFalsy();
      done();
    });
  });
});

Run the tests again and make sure it works.

Run the application

Now let’s try to run the application.

Open a terminal, and go to the root folder of the project, and execute the following command.

>npm run start
> nestjs-sample@0.0.1 start D:\souyanglabs\nestjs-sample
> nest start

[Nest] 9956   - 06/13/2020, 12:04:50 PM   [NestFactory] Starting Nest application...
[Nest] 9956   - 06/13/2020, 12:04:50 PM   [InstanceLoader] AppModule dependencies initialized +16ms
[Nest] 9956   - 06/13/2020, 12:04:50 PM   [InstanceLoader] PostModule dependencies initialized +1ms
[Nest] 9956   - 06/13/2020, 12:04:50 PM   [RoutesResolver] AppController {}: +10ms
[Nest] 9956   - 06/13/2020, 12:04:50 PM   [RouterExplorer] Mapped {, GET} route +4ms
[Nest] 9956   - 06/13/2020, 12:04:50 PM   [RoutesResolver] PostController {/posts}: +1ms
[Nest] 9956   - 06/13/2020, 12:04:50 PM   [RouterExplorer] Mapped {/posts, GET} route +1ms
[Nest] 9956   - 06/13/2020, 12:04:50 PM   [RouterExplorer] Mapped {/posts/:id, GET} route +2ms
[Nest] 9956   - 06/13/2020, 12:04:50 PM   [RouterExplorer] Mapped {/posts, POST} route +1ms
[Nest] 9956   - 06/13/2020, 12:04:50 PM   [RouterExplorer] Mapped {/posts/:id, PUT} route +2ms
[Nest] 9956   - 06/13/2020, 12:04:50 PM   [RouterExplorer] Mapped {/posts/:id, DELETE} route +2ms
[Nest] 9956   - 06/13/2020, 12:04:50 PM   [NestApplication] Nest application successfully started +7ms

When it is started up, it serves at http://localhost:3000.

Let’s test the exposed APIs via curl:

>curl http://localhost:3000/posts
[{"id":1,"title":"Generate a NestJS API","content":"content","createdAt":"2020-06-13T04:20:21.920Z"},{"id":2,"title":"Create RESTful CRUD Web Service","content":"content","createdAt":"2020-06-13T04:20:21.920Z"},{"id":3,"title":"Connect to Mongodb Database","content":"content","createdAt":"2020-06-13T04:20:21.920Z"}]

>curl http://localhost:3000/posts/1
{"id":1,"title":"Generate a NestJS API","content":"content","createdAt":"2020-06-13T04:20:21.920Z"}

>curl http://localhost:3000/posts -d "{\"title\":\"new post\",\"content\":\"content of my new post\"}" -H "Content-Type:application/json" -X POST
[{"id":1,"title":"Generate a NestJS API","content":"content","createdAt":"2020-06-13T04:20:21.920Z"},{"id":2,"title":"Create RESTful CRUD Web Service","content":"content","createdAt":"2020-06-13T04:20:21.920Z"},{"id":3,"title":"Connect to Mongodb Database","content":"content","createdAt":"2020-06-13T04:20:21.920Z"},{"title":"new post","content":"content of my new post","id":4,"createdAt":"2020-06-13T04:20:52.526Z"}]

>curl http://localhost:3000/posts/1 -d "{\"title\":\"updated post\",\"content\":\"content of my upated post\"}" -H "Content-Type:application/json" -X PUT
[{"id":1,"title":"updated post","content":"content of my updated post","createdAt":"2020-06-13T04:20:21.920Z","updatedAt":"2020-06-13T04:21:08.259Z"},{"id":2,"title":"Create RESTful CRUD Web Service","content":"content","createdAt":"2020-06-13T04:20:21.920Z"},{"id":3,"title":"Connect to Mongodb Database","content":"content","createdAt":"2020-06-13T04:20:21.920Z"},{"title":"new post","content":"content of my new post","id":4,"createdAt":"2020-06-13T04:20:52.526Z"}]

>curl http://localhost:3000/posts/1 -X DELETE
true

>curl http://localhost:3000/posts
[{"id":2,"title":"Create RESTful CRUD Web Service","content":"content","createdAt":"2020-06-13T04:20:21.920Z"},{"id":3,"title":"Connect to Mongodb Database","content":"content","createdAt":"2020-06-13T04:20:21.920Z"},{"title":"new post","content":"content of my new post","id":4,"createdAt":"2020-06-13T04:20:52.526Z"}]

Go back to top

Other Sections

Thanks

If you like the examples, share a star