NestJS is often praised for its scalability and enterprise-grade architecture, but that can sometimes make it feel intimidating for beginners. You might hear terms like "Dependency Injection," "Modules," and "Decorators" and wonder if you need a complex database setup just to get started.
You don't.
In this tutorial, we're going to strip away the complexity and build a tiny, in-memory Todo API using NestJS. We'll stick to a minimal MVC (Model-View-Controller) style patternβthough since this is an API, think of it as DTO-Service-Controller. No database, no ORM, just pure TypeScript logic.
By the end of this guide, you'll understand how data flows through a NestJS application.
π The Project Structure
We are keeping things flat and simple. Here is the file structure we will be working with:
src/
βββ todo/
β βββ dto/
β β βββ create-todo.dto.ts
β βββ todo.controller.ts
β βββ todo.service.ts
βββ app.module.ts
βββ main.ts
Step 1: Define the Data Shape (DTO)
In NestJS, it's best practice to define what your data looks like using a DTO (Data Transfer Object). This ensures that when a user sends data to our API, we know exactly what fields to expect.
Create src/todo/dto/create-todo.dto.ts:
export class CreateTodoDto {
title: string;
description?: string;
}
Note: In a production app, you might use class-validator to enforce rules (like making title required), but for this tiny app, a simple class definition is enough to define the type.
Step 2: The Service (Business Logic)
The Service is where the magic happens. It holds our data and performs operations on it. Since we aren't using a database, we'll store our todos in a simple array in memory.
Create src/todo/todo.service.ts:
import { Injectable } from '@nestjs/common';
import { CreateTodoDto } from './dto/create-todo.dto';
export interface Todo {
id: number;
title: string;
description?: string;
}
@Injectable()
export class TodoService {
private todos: Todo[] = [];
private idCounter = 1;
findAll(): Todo[] {
return this.todos;
}
create(createTodoDto: CreateTodoDto): Todo {
const newTodo: Todo = {
id: this.idCounter++,
...createTodoDto,
};
this.todos.push(newTodo);
return newTodo;
}
delete(id: number): boolean {
const initialLength = this.todos.length;
this.todos = this.todos.filter(todo => todo.id !== id);
return this.todos.length < initialLength;
}
}
Key Concept: The @Injectable() decorator tells NestJS that this class can be managed by the dependency injection system. This allows us to inject this service into our controller later.
Step 3: The Controller (Routes)
The Controller listens for incoming HTTP requests (GET, POST, DELETE) and delegates the work to the Service. It doesn't care how the data is saved; it just cares about receiving the request and sending back a response.
Create src/todo/todo.controller.ts:
import { Controller, Get, Post, Delete, Body, Param, HttpCode, HttpStatus } from '@nestjs/common';
import { TodoService } from './todo.service';
import type { Todo } from './todo.service';
import { CreateTodoDto } from './dto/create-todo.dto';
@Controller('todos')
export class TodoController {
constructor(private readonly todoService: TodoService) {}
@Get()
findAll(): Todo[] {
return this.todoService.findAll();
}
@Post()
create(@Body() createTodoDto: CreateTodoDto): Todo {
return this.todoService.create(createTodoDto);
}
@Delete(':id')
@HttpCode(HttpStatus.OK)
delete(@Param('id') id: string): { message: string } {
const deleted = this.todoService.delete(parseInt(id, 10));
if (!deleted) {
return { message: 'Todo not found' };
}
return { message: 'Todo deleted successfully' };
}
}
Key Concepts:
-
@Controller('todos'): Sets the base route for all endpoints in this class to/todos -
@Get(),@Post(),@Delete(): Decorators that map methods to specific HTTP verbs -
@Body(): Extracts the JSON payload from the request -
@Param('id'): Extracts theidparameter from the URL (e.g.,/todos/1)
Step 4: The Module
NestJS applications are organized into modules. The module acts as a container that tells NestJS which controllers and services belong together.
Update src/app.module.ts:
import { Module } from '@nestjs/common';
import { TodoController } from './todo/todo.controller';
import { TodoService } from './todo/todo.service';
@Module({
controllers: [TodoController],
providers: [TodoService],
})
export class AppModule {}
Since this is a tiny app, we are putting everything directly into the root AppModule. In larger apps, you would create a separate TodoModule and import it here.
Step 5: The Entry Point
Finally, we need to bootstrap the application. This is the standard boilerplate code generated by the Nest CLI.
Update src/main.ts:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
console.log('π Application is running on: http://localhost:3000');
}
bootstrap();
π§ͺ Let's Test It!
Start your development server by running:
npm run start:dev
Now, let's interact with our API using cURL, Postman, or your browser.
1. β Create a Todo
Send a POST request to http://localhost:3000/todos with a JSON body:
{
"title": "Learn NestJS",
"description": "Build a simple Todo app"
}
2. π Get All Todos
Send a GET request to http://localhost:3000/todos
You should see:
[
{
"id": 1,
"title": "Learn NestJS",
"description": "Build a simple Todo app"
}
]
3. β Delete a Todo
Send a DELETE request to http://localhost:3000/todos/1
You should receive:
{
"message": "Todo deleted successfully"
}
If you GET the todos again, the list will be empty!
π Conclusion
You've just built a functional REST API with NestJS!
This minimal example demonstrates the core architectural pattern of NestJS:
- DTOs define the data shape.
- Services handle business logic and data.
- Controllers handle HTTP requests.
- Modules wire everything together.
Enjoy your clean, tiny NestJS Todo app! β¨
Made with β€οΈ for beginners
Top comments (0)