Hey everyone! What’s up? Let’s kick off a series of posts about NestJS. The idea here is to build a really simple task management service — basically a to-do list. And, of course, we’ll use a bunch of things we use every day when building services.
What is NestJS
NestJS is a great framework for building Node.js applications with TypeScript. It’s super modular and extensible. For the HTTP server, it uses the well-known ExpressJS under the hood — but if you need high performance, you can switch to Fastify!
The coolest features of Nest are its module system, dependency injection, decorator usage, and how easy it is to extend the code. The ecosystem has packages for everything: databases (MySQL, Postgres, MongoDB), queues, microservices. Let’s get started!
Getting started with NestJS
To create our service, we first need to install the Nest CLI.
# pnpm
pnpm add -g @nestjs/cli
# npm
npm install -g @nestjs/cli
# yarn
yarn global add -g @nestjs/cli
After installing the Nest CLI, let’s create our Nest project:
nest new todo-list-service
You’ll be asked which package manager you want to use. I personally prefer pnpm, since it’s faster than the others. Once the process finishes, you should see the created project inside the folder with this structure:
todo-list-service
|- src
|- app.controller.spec.ts
|- app.controller.ts
|- app.module.ts
|- app.service.ts
|- main.ts
|- tests
|- package.json
|- package-lock.json
|- tsconfig.json
|-...configuration-files
First endpoint
When the project is created, the files main.ts, app.module.ts, app.controller.ts, and app.service.ts are generated. The main.ts file is where the application is created and started. This happens inside the bootstrap method. To create the application, it calls AppModule, a class implemented in the app.module.ts file. In this class, using the @Module() decorator, we define which controllers our application has and which providers it uses. We can also import other modules.
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController], // Controllers should be imported here
providers: [AppService], // Providers should be imported here
})
export class AppModule {}
To create an endpoint, we can go to the app.controller.ts file. In this file we have an AppController class with the @Controller() decorator. Each endpoint is a method of that class, using one of these decorators: @Get(), @Post, @Put, @Delete, @Patch, @Head, @Options, @Trace.
When the application is created, we already have a GET at / that returns Hello World!. Let’s add one more GET that will be our health check:
import { Controller, Get } from '@nestjs/common';
@Controller()
export class AppController {
// Already implemented
@Get()
getHello(): string {
return 'Hello World!';
}
@Get('health')
getHealth() {
return { status: 200, message: 'UP' };
}
}
Inside the @Controller decorator, and in the decorators that map to the HTTP verb, we can define what the route will be.
For example, if we had a to-do’s API, the controller could be @Controller("todos"). Every method mapped as an HTTP request will respond using a route that starts with /todos. For a GET that fetches a to-do by id, the get decorator would be @Get(":id") and the final route would be /todos/:id.
If the controller decorator has no prefix at all, the route will respond starting from the root /.
Running the service
Now that we’ve created our first endpoint, let’s run the service and make a GET request to the API. To run the service, we’ll use the start:dev script defined in package.json, calling it with a Node package manager:
# pnpm
pnpm start:dev
# npm
npm run start:dev
# yarn
yarn start:dev
You should see something like this in the terminal:
[12:43:37 PM] File change detected. Starting incremental compilation...
[12:43:37 PM] Found 0 errors. Watching for file changes.
[Nest] 3173 - 02/07/2025, 12:43:38 PM LOG [NestFactory] Starting Nest application...
[Nest] 3173 - 02/07/2025, 12:43:38 PM LOG [InstanceLoader] AppModule dependencies initialized +2ms
[Nest] 3173 - 02/07/2025, 12:43:38 PM LOG [RoutesResolver] AppController {/}: +0ms
[Nest] 3173 - 02/07/2025, 12:43:38 PM LOG [RouterExplorer] Mapped {/, GET} route +0ms
[Nest] 3173 - 02/07/2025, 12:43:38 PM LOG [RouterExplorer] Mapped {/error, POST} route +0ms
[Nest] 3173 - 02/07/2025, 12:43:38 PM LOG [NestApplication] Nest application successfully started +0ms
Nest’s default port is 3000, so the service will most likely respond to HTTP calls at http://localhost:3000. With the service running, let’s make our first request to it. To do that, just call it via curl:
curl -v http://localhost:3000/health
And you should see something like this:
* Trying 127.0.0.1:3000...
* Connected to localhost (127.0.0.1) port 3000 (#0)
> GET /health HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.81.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Access-Control-Allow-Origin: *
< Content-Type: application/json; charset=utf-8
< Content-Length: 29
< ETag: W/"1d-IDhexl3N5aiBtUkBl0nBxaLgj3A"
< Date: Fri, 07 Feb 2025 15:49:45 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
<
* Connection #0 to host localhost left intact
{"status":200,"message":"UP"}%
Awesome! In the next post, we’ll add services and modules to our service.
Enjoyed the article? Share it on social media.