In this post, I will demonstrate how to kickstart a simple RESTful APIs with NestJS from a newbie's viewpoint.
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.
- A lot of NestJS concepts are heavily inspired by the effort of the popular frameworks in the world, esp. Angular .
- Nestjs hides the complexities of web programming in NodeJS, it provides a common abstraction of the web request handling, you are free to choose Expressjs or Fastify as the background engine.
- Nestjs provides a lot of third party project integrations, from database operations, such as Mongoose, TypeORM, etc. to Message Brokers, such as Redis, RabbitMQ, etc.
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.
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 project via:
nest new nestjs-sample
Open it in your favorite IDEs, such as Intellij WebStorm or VSCode.
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.
- src/main.ts is the entry file of this application.
- src/app* is the top level component in a nest application.
- There is an app.module.ts is a Nestjs
Module
which is similar with AngularNgModule
, and used to organize codes in the logic view. - The app.service.ts is an
@Injectable
component, similar with the service in Angular or Spring's Service, it is used for handling business logic. A service is annotated with@Injectable
. - The app.controller.ts is the controller of MVC, to handle incoming request, and responds the handled result back to client. The annotatoin
@Controller()
is similar with Spring MVC's@Controller
. - The app.controller.spec.ts is test file for app.controller.ts. Nestjs uses Jest as testing framework.
- There is an app.module.ts is a Nestjs
- test folder is for storing e2e test files.
I will reuse the concept I've used in the former examples - the blogging posts.
- GET /posts - Get all posts
- GET /posts/id - Get post by id
- POST /posts - Save a post
- PUT /posts/id - Update the certain post specified by id
- DELETE /posts/id - Delete a post
In the next steps, we will create:
- A new module
PostModule
to organize all these features. - A
Post
interface to present thePost
entity resource. - A
PostService
component to serve the data for controller. In this post, we use a dummy data storage temporarily, and we will replace it with a real MongoDB in the future post. - A
PostController
to expose APIs which is responsible for handling incoming requests.
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 project',
content: 'content',
createdAt: new Date(),
},
{
id: 2,
title: 'Create CRUD RESTful APIs',
content: 'content',
createdAt: new Date(),
},
{
id: 3,
title: 'Connect to MongoDB',
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
> [email protected] test D:\hantsylabs\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 project');
},
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: 'test title',
content: 'test 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: 'test title',
content: 'test content',
createdAt: new Date(),
})
.pipe(take(4), toArray())
.subscribe({
next: data => {
expect(data.length).toBe(3);
expect(data[0].title).toBe('test title');
expect(data[0].content).toBe('test 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.
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
constructor
, we addPostService
as its arguments, thePostService
component will be injected automatically. - The
Controller
annotation indicates it is a controller, and it uses/posts
as base uri all methods. - The
Get
,Post
,Put
, andDelete
on methods are used for handling different HTTP methods. - The
@Param
binds the path parameters (:id
) to the method arguments, and there isParseIntPipe
applied in the second arguments of Param. Pipe is a Angular concept, here it is more close to Spring's converters and validators. @Body
parses the request body into aPost
object.
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:'test title', content:'test 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:'test title', content:'test content'};
controller.updatePost(1, post).subscribe(data => {
expect(data.length).toBe(3);
expect(data[0].title).toEqual('test title');
expect(data[0].content).toEqual('test 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.
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
> [email protected] start D:\hantsylabs\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 project","content":"content","createdAt":"2020-06-13T04:20:21.920Z"},{"id":2,"title":"Create CRUD RESTful APIs","content":"content","createdAt":"2020-06-13T04:20:21.920Z"},{"id":3,"title":"Connect to MongoDB","content":"content","createdAt":"2020-06-13T04:20:21.920Z"}]
>curl http://localhost:3000/posts/1
{"id":1,"title":"Generate a NestJS project","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 project","content":"content","createdAt":"2020-06-13T04:20:21.920Z"},{"id":2,"title":"Create CRUD RESTful APIs","content":"content","createdAt":"2020-06-13T04:20:21.920Z"},{"id":3,"title":"Connect to MongoDB","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 upated post","createdAt":"2020-06-13T04:20:21.920Z","updatedAt":"2020-06-13T04:21:08.259Z"},{"id":2,"title":"Create CRUD RESTful APIs","content":"content","createdAt":"2020-06-13T04:20:21.920Z"},{"id":3,"title":"Connect to MongoDB","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 CRUD RESTful APIs","content":"content","createdAt":"2020-06-13T04:20:21.920Z"},{"id":3,"title":"Connect to MongoDB","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"}]
In the further post, we will connect to a real database to replace the dummy data service.