Build a Todo REST API with NestJS
Build a CRUD REST API with NestJS, MongoDB Atlas, and input validation.
Introduction
30 Second Summary
Every app you use has an API behind it. When you check off a task in your favorite productivity app, a request flies to a server that updates a database. Understanding how to build that backend is one of the most valuable skills in software development.
In this project, you will build a Todo REST API from scratch using NestJS and MongoDB. Your API will support creating, reading, updating, and deleting todo items, with proper input validation and error handling.
What You'll Build
A fully functional REST API that manages todo items, connected to a cloud MongoDB database and protected by automatic request validation.
By the end of this project, you'll have:
- A NestJS project with a dedicated Todos module containing a controller, service, schema, and DTOs.
- A MongoDB Atlas free cluster connected to your application via Mongoose.
- Five working API endpoints for full CRUD operations, protected by a global validation pipeline that rejects malformed requests.
- Secret Mission: Add filtering by completion status and a toggle-complete endpoint.
Are there any prerequisites?
You should be comfortable with TypeScript basics and have Node.js installed on your machine. A code editor like VS Code is also needed.
Set Up the Project and Database
Before you can build API endpoints, you need a project structure and a database to store data in. NestJS provides a CLI that scaffolds a ready-to-run application with modules, controllers, and services already wired together.
In this step, you'll install the NestJS CLI, scaffold your project, and set up a free MongoDB Atlas cluster to serve as your cloud database. You'll also install the packages needed to connect NestJS to MongoDB and validate incoming requests.
In this step, get ready to:
- Install the NestJS CLI and scaffold a new project.
- Create a free MongoDB Atlas cluster and get a connection string.
- Install MongoDB and validation dependencies.
Install the NestJS CLI and scaffold a new project
The NestJS CLI generates a complete project structure in one command. You'll install it globally so you can use it from any directory.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Install the NestJS CLI globally by running this command:
npm install -g @nestjs/cli
What does this command do?
The -g flag installs the package globally on your machine. This means you can run nest commands from any folder, not just this project.
Seeing a permission error?
On macOS or Linux, you may need to prefix the command with sudo if you get an EACCES error. On Windows, try running your terminal as Administrator.
Still stuck? Help me fix the npm global install permission error.
- Scaffold your new NestJS project by running this command:
nest new todo-api
What will the CLI ask?
The CLI will prompt you to choose a package manager. It shows three options: npm, yarn, and pnpm.
Select npm using your arrow keys and press Enter. The CLI will then install all dependencies automatically.
- Once the scaffolding finishes, move into your new project folder by running:
cd todo-api
- Start the development server to confirm everything works by running:
npm run start:dev
Wait a few seconds for the server to compile and start.
- Open your browser and navigate to http://localhost:3000.
✔️ I see Hello World!
Your NestJS project is running correctly. Leave the terminal running and move on to the next substep.
ⓧ I see an error or nothing loads
- Check your terminal for compilation errors. A typo in a file could prevent startup.
- Make sure you ran cd todo-api before running the start command.
- If port 3000 is already in use, stop whatever else is using it and try again.
Need more help? Help me troubleshoot my NestJS dev server not starting.
- Stop the dev server by pressing Ctrl+C in your terminal.
Create a MongoDB Atlas free cluster
Your API needs a database to persist todo items. MongoDB Atlas provides a free cloud-hosted MongoDB cluster that requires zero local installation.
- Open your browser and go to https://cloud.mongodb.com.
- Sign up for a new account or log in if you already have one.
- If prompted, create a new project. Give it any name you like.
- Click Create to start creating a new cluster.
- Select the Free cluster option (M0 tier).
- Choose any cloud provider (AWS, Google Cloud, or Azure) and a region close to you.
- Click Create Deployment. The cluster takes less than 15 seconds to become ready.
What is MongoDB Atlas?
MongoDB Atlas is a fully managed cloud database service. The Free (M0) tier gives you 512 MB of storage at no cost, which is more than enough for this project.
You get one Free cluster per Atlas project. It runs on shared infrastructure and supports up to 500 concurrent connections.
After the cluster is created, Atlas walks you through a Security Quickstart wizard.
- Create a database user by entering a Username and Password.
- Write down the username and password somewhere safe. You will need them for the connection string.
- Click Create Database User.
- Under the IP Access List section, click Add My Current IP Address.
- Click Finish and Close.
Now you need the connection string so your app can connect to this cluster.
- On the cluster overview page, click Connect.
- In the connection method step, select Drivers.
- Copy the connection string displayed on the page. It looks like this:
mongodb+srv://<username>:<password>@<cluster-url>/?retryWrites=true&w=majority
Save your connection string
Paste this connection string somewhere you can access it easily (a notes app or a text file). Replace <username> and <password> with the database user credentials you just created.
Install MongoDB and validation dependencies
Your project needs packages to connect to MongoDB and to validate incoming API requests. Mongoose is the library that maps your TypeScript classes to MongoDB documents.
- In your terminal (still inside the todo-api folder), install all five packages by running:
npm install @nestjs/mongoose mongoose class-validator class-transformer @nestjs/mapped-types
What are these packages?
- @nestjs/mongoose provides NestJS-specific decorators and module integration for Mongoose.
- mongoose is the MongoDB object modeling library that handles database connections and queries.
- class-validator adds decorators to validate incoming request data.
- class-transformer transforms plain objects into class instances so validators can run.
- @nestjs/mapped-types lets you create partial or derived DTO types without repeating yourself.
- Verify the packages installed by opening package.json and checking the dependencies section.
You should see all five packages listed: @nestjs/mongoose, mongoose, class-validator, class-transformer, and @nestjs/mapped-types.
Install command failed?
Make sure you are inside the todo-api folder before running the install command. If you see network errors, check your internet connection and try again.
Need help? Help me fix my npm install error.
Your project is scaffolded, your database is ready, and your packages are installed. Next up, you'll define the Todo schema and wire everything together.
Define the Todo Schema
Your NestJS project is scaffolded and your MongoDB Atlas cluster is live. Now you need to define what a "todo" actually looks like in your database.
NestJS organizes code into modules, each responsible for a single feature. In this step, you'll generate a Todos module and define a Mongoose schema that maps your Todo data to MongoDB.
In this step, get ready to:
- Generate the Todos module, controller, and service using the NestJS CLI.
- Define the Todo schema with Mongoose decorators.
- Connect the application to MongoDB Atlas.
Create the Todos module structure
The NestJS CLI can generate boilerplate files for you and automatically register them in the right places. You'll use it to scaffold a module, controller, and service for the Todos feature.
- Stop the dev server if it's still running by pressing Ctrl+C in the terminal.
- Generate the Todos module by running this command:
nest generate module todos
- Generate the Todos controller by running:
nest generate controller todos
- Generate the Todos service by running:
nest generate service todos
What did these commands do?
Each nest generate command creates the relevant file AND auto-registers it. The module command added TodosModule to your app.module.ts imports array. The controller and service commands registered themselves inside todos.module.ts.
This automatic wiring is part of NestJS's dependency injection system. You don't have to manually import everything yourself.
- Create the schemas directory and an empty schema file by running these commands:
mkdir -p src/todos/schemas
touch src/todos/schemas/todo.schema.ts
- Confirm the Todos module structure exists by running:
ls src/todos/
You should see todos.module.ts, todos.controller.ts, todos.service.ts, and the schemas/ folder in the output.
Command not found for nest?
If your terminal doesn't recognize nest, the CLI may not be installed globally. Re-run npm install -g @nestjs/cli from Step 1, then try again.
Help me fix a 'nest: command not found' error in my terminal.
Define the Todo schema with Mongoose decorators
A schema tells Mongoose the shape of your documents. Each Todo will have a title, an optional description, and a completed flag.
- Open src/todos/schemas/todo.schema.ts in VS Code and paste the following code:
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument } from 'mongoose';
// Type alias for a fully hydrated Mongoose document
export type TodoDocument = HydratedDocument<Todo>;
// Marks this class as a Mongoose schema with automatic timestamps
@Schema({ timestamps: true })
export class Todo {
// Title is required for every todo
@Prop({ required: true })
title: string;
// Description is optional
@Prop()
description: string;
// Defaults to false when a new todo is created
@Prop({ default: false })
completed: boolean;
}
// Generates the actual Mongoose schema from the class
export const TodoSchema = SchemaFactory.createForClass(Todo);
What does this code do?
The @Schema({ timestamps: true }) decorator marks this class as a Mongoose schema definition and tells MongoDB to auto-add createdAt and updatedAt fields to every document.
Each @Prop() decorator defines a property on the document. Options like required: true and default: false set validation and default values at the database level.
The TodoDocument type and TodoSchema export are used by NestJS to register and interact with this schema elsewhere in the application.
- Save todo.schema.ts.
✔️ Awesome, I've got everything!
Great. Double check you've saved the file before moving on.
ⓧ I'd like to double check the full code
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument } from 'mongoose';
export type TodoDocument = HydratedDocument<Todo>;
@Schema({ timestamps: true })
export class Todo {
@Prop({ required: true })
title: string;
@Prop()
description: string;
@Prop({ default: false })
completed: boolean;
}
export const TodoSchema = SchemaFactory.createForClass(Todo);
Register the schema and connect to MongoDB
Your schema exists, but NestJS doesn't know about it yet. You need to register it in the Todos module and connect the entire application to your Atlas cluster.
- Open src/todos/todos.module.ts in VS Code.
- Replace the entire file contents with the following code:
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { TodosController } from './todos.controller';
import { TodosService } from './todos.service';
import { Todo, TodoSchema } from './schemas/todo.schema';
@Module({
// Registers the Todo schema so Mongoose can use it in this module
imports: [MongooseModule.forFeature([{ name: Todo.name, schema: TodoSchema }])],
controllers: [TodosController],
providers: [TodosService],
})
export class TodosModule {}
What does forFeature do?
MongooseModule.forFeature() registers which schemas belong to this module. It creates a Mongoose model for Todo that you can inject into services later.
Think of forFeature as connecting a specific schema to a specific module. The forRoot method (which you'll add next) handles the global database connection.
- Save todos.module.ts.
- Now open src/app.module.ts.
- Replace the entire file contents with the following code, substituting your Atlas connection string details:
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { TodosModule } from './todos/todos.module';
@Module({
imports: [
// Connects the app to your MongoDB Atlas cluster
MongooseModule.forRoot(
'mongodb+srv://your-username:your-password@your-cluster-url/todo-app?retryWrites=true&w=majority',
),
TodosModule,
],
})
export class AppModule {}
What do I replace in the connection string?
Replace your-username and your-password with the database user credentials you created in Atlas. Replace your-cluster-url with the cluster address from your connection string (it looks something like cluster0.abc123.mongodb.net).
The /todo-app at the end of the URL tells MongoDB which database to use. MongoDB creates it automatically on the first write.
- Save app.module.ts.
- Start the dev server by running:
npm run start:dev
The app should start without any connection errors in the console output. If the connection to Atlas succeeds, you'll see the normal NestJS startup log with no errors.
Seeing a MongooseServerSelectionError?
This error means the app cannot reach your Atlas cluster. Check these common causes:
- Your connection string has a typo in the username, password, or cluster URL. Copy the string again from Atlas and double-check each part.
- Your current IP address is not in the Atlas Network Access list. Go back to Atlas, navigate to **Network Access**, and verify your IP is whitelisted.
- The password contains special characters that need URL encoding. If your password has characters like @ or #, replace them with their percent-encoded equivalents.
Help me debug a MongooseServerSelectionError when connecting my NestJS app to MongoDB Atlas.
✔️ Awesome, I've got everything!
Great. Make sure both files are saved before moving on.
ⓧ I'd like to double check the full code
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { TodosController } from './todos.controller';
import { TodosService } from './todos.service';
import { Todo, TodoSchema } from './schemas/todo.schema';
@Module({
imports: [MongooseModule.forFeature([{ name: Todo.name, schema: TodoSchema }])],
controllers: [TodosController],
providers: [TodosService],
})
export class TodosModule {}
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { TodosModule } from './todos/todos.module';
@Module({
imports: [
MongooseModule.forRoot(
'mongodb+srv://your-username:your-password@your-cluster-url/todo-app?retryWrites=true&w=majority',
),
TodosModule,
],
})
export class AppModule {}
Your data layer is wired up. NestJS knows the shape of a Todo, and the app can talk to your Atlas cluster. Next up, you'll create DTOs and a validation pipeline to control what data enters your API.
Create DTOs and Enable Validation
Your Todo schema tells MongoDB how to store data. But you also need a way to control what data is allowed into your API in the first place.
Data Transfer Objects (DTOs) define the exact shape of incoming requests. Combined with a validation pipe, they automatically reject malformed data before it ever reaches your database. In this step, you'll create DTOs for your todo endpoints and wire up NestJS's global validation.
In this step, get ready to:
- Create DTOs that define what a valid todo request looks like.
- Enable a global validation pipeline that rejects bad requests automatically.
Create the Data Transfer Objects
A DTO is a class that defines the shape of data flowing through your API. Instead of accepting any random JSON body, your DTOs use decorators from class-validator to enforce rules on each field.
- Create the src/todos/dto/ directory and a new file called create-todo.dto.ts inside it by running:
mkdir src/todos/dto
touch src/todos/dto/create-todo.dto.ts
- Add the following code to src/todos/dto/create-todo.dto.ts:
import { IsString, IsNotEmpty, IsOptional, IsBoolean } from 'class-validator';
export class CreateTodoDto {
// Title is required and must be a string
@IsString()
@IsNotEmpty()
title: string;
// Description is optional but must be a string if provided
@IsString()
@IsOptional()
description?: string;
// Completed is optional but must be a boolean if provided
@IsBoolean()
@IsOptional()
completed?: boolean;
}
What does this code do?
Each property has validation decorators stacked on top of it that act as rules:
- @IsString() ensures the value is a string. @IsNotEmpty() rejects empty strings or missing values. Together, these make title required.
- @IsOptional() means the field can be omitted entirely. If it IS provided, the other decorator (@IsString() or @IsBoolean()) still validates the value.
- The ? after description and completed marks them as optional in TypeScript's type system.
- Save create-todo.dto.ts.
You won't see a change yet. The next file uses this DTO as its foundation.
- Create a new file called update-todo.dto.ts in the same src/todos/dto/ directory by running:
touch src/todos/dto/update-todo.dto.ts
- Add the following code to src/todos/dto/update-todo.dto.ts:
import { PartialType } from '@nestjs/mapped-types';
import { CreateTodoDto } from './create-todo.dto';
// Makes all CreateTodoDto fields optional for partial updates
export class UpdateTodoDto extends PartialType(CreateTodoDto) {}
What does PartialType do?
PartialType takes your CreateTodoDto and creates a new class where every field is optional. This is perfect for PATCH requests where the client only sends the fields they want to change.
Without PartialType, you'd have to duplicate all your validation decorators and manually mark everything as optional. One line handles it all.
- Save update-todo.dto.ts.
- Confirm both files exist by running:
ls src/todos/dto/
You should see create-todo.dto.ts and update-todo.dto.ts in the output.
Don't see both files?
- Make sure you created the dto directory inside src/todos/, not at the project root.
- Check for typos in the filenames. Both should end with .dto.ts.
Still stuck? Help me find my DTO files in the todos directory.
✔️ Awesome, I've got everything!
Great. Double check you've saved both files.
ⓧ I'd like to double check the full code
import { IsString, IsNotEmpty, IsOptional, IsBoolean } from 'class-validator';
export class CreateTodoDto {
@IsString()
@IsNotEmpty()
title: string;
@IsString()
@IsOptional()
description?: string;
@IsBoolean()
@IsOptional()
completed?: boolean;
}
import { PartialType } from '@nestjs/mapped-types';
import { CreateTodoDto } from './create-todo.dto';
export class UpdateTodoDto extends PartialType(CreateTodoDto) {}
Enable the global ValidationPipe
DTOs define the rules, but NestJS needs to be told to actually enforce them. The ValidationPipe is a built-in pipe that automatically validates incoming request bodies against your DTO decorators.
- In VS Code, open src/main.ts.
- Find this line:
import { NestFactory } from '@nestjs/core';
- Replace the entire contents of main.ts with the following updated version:
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Validate all incoming requests against DTO rules
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
What does this change?
- import { ValidationPipe } from '@nestjs/common' brings in NestJS's built-in validation pipe.
- app.useGlobalPipes(new ValidationPipe()) tells NestJS to run validation on every incoming request across the entire app. Any request body that fails DTO rules gets rejected with a 400 status code before your handler ever runs.
- Save main.ts.
- Return to your terminal and run the dev server:
npm run start:dev
The server should start without errors. You should see the NestJS application bootstrapping in the console output.
- Test that validation works by sending a POST request with an empty body:
curl -X POST http://localhost:3000/todos -H "Content-Type: application/json" -d '{}'
You should receive a 400 error with validation messages indicating that title is required.
Seeing a different error?
- If you see a connection error, make sure the dev server is still running in another terminal tab.
- If you see Cannot POST /todos, this is expected if the POST handler is not wired up yet. The validation will kick in once you implement the controller routes in the next step.
- If you see a "class-validator" module error, double check that you installed class-validator and class-transformer in Step 1. Run npm install class-validator class-transformer if needed.
Still stuck? Help me debug my NestJS ValidationPipe setup.
✔️ Awesome, I've got everything!
Great. Double check you've saved main.ts.
ⓧ I'd like to double check the full code
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
Your API now has a safety net. Any request that doesn't match your DTO rules gets rejected automatically. Next up, you'll implement the actual CRUD operations that create, read, update, and delete todos.
Implement CRUD Operations
Your DTOs and validation pipeline are ready to guard incoming requests. Now it is time to write the actual logic that creates, reads, updates, and deletes todos in your database.
In NestJS, a service handles all database operations while a controller maps incoming HTTP requests to the right service method. You will build both in this step, then test the full flow with curl commands.
In this step, get ready to:
- Implement the TodosService with all CRUD methods.
- Wire up the TodosController with route handlers.
- Test the full CRUD flow with curl.
Implement the TodosService with all CRUD methods
The service is where all database interaction lives. NestJS uses dependency injection to provide the Mongoose model to your service class automatically.
- In VS Code, open src/todos/todos.service.ts and replace its contents with this code:
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Todo } from './schemas/todo.schema';
import { CreateTodoDto } from './dto/create-todo.dto';
import { UpdateTodoDto } from './dto/update-todo.dto';
@Injectable()
export class TodosService {
// Inject the Mongoose model for the Todo schema
constructor(@InjectModel(Todo.name) private todoModel: Model<Todo>) {}
// Create a new todo document from the validated DTO
async create(createTodoDto: CreateTodoDto): Promise<Todo> {
const createdTodo = new this.todoModel(createTodoDto);
return createdTodo.save();
}
// Return all todo documents from the database
async findAll(): Promise<Todo[]> {
return this.todoModel.find().exec();
}
// Find a single todo by its MongoDB _id
async findOne(id: string): Promise<Todo> {
const todo = await this.todoModel.findById(id).exec();
if (!todo) {
throw new NotFoundException(`Todo with ID "${id}" not found`);
}
return todo;
}
// Update a todo's fields and return the updated document
async update(id: string, updateTodoDto: UpdateTodoDto): Promise<Todo> {
const updatedTodo = await this.todoModel
.findByIdAndUpdate(id, updateTodoDto, { new: true })
.exec();
if (!updatedTodo) {
throw new NotFoundException(`Todo with ID "${id}" not found`);
}
return updatedTodo;
}
// Delete a todo by its _id and return the deleted document
async remove(id: string): Promise<Todo> {
const deletedTodo = await this.todoModel.findByIdAndDelete(id).exec();
if (!deletedTodo) {
throw new NotFoundException(`Todo with ID "${id}" not found`);
}
return deletedTodo;
}
}
What does this code do?
This service class encapsulates all database logic for todos. Here are the key patterns:
- @InjectModel(Todo.name) tells NestJS to inject the Mongoose model so you can query the todos collection.
- create() instantiates a new model document from the DTO and calls .save() to persist it to MongoDB.
- findByIdAndUpdate() with { new: true } returns the updated document instead of the original.
- NotFoundException automatically returns a 404 HTTP response when a todo is not found.
- Save todos.service.ts.
- Check your terminal where npm run start:dev is running. The app should recompile without any TypeScript errors.
Seeing TypeScript errors?
- Make sure all six import lines at the top are present. A missing import for NotFoundException or Model causes compile failures.
- Check that @InjectModel(Todo.name) uses Todo.name (the class property), not a raw string like 'Todo'.
- Confirm the DTO import paths point to ./dto/create-todo.dto and ./dto/update-todo.dto with no typos.
Why is my service not compiling?
✔️ Awesome, I've got everything!
Great. Double check you have saved todos.service.ts before moving on.
ⓧ I'd like to double check the full code
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Todo } from './schemas/todo.schema';
import { CreateTodoDto } from './dto/create-todo.dto';
import { UpdateTodoDto } from './dto/update-todo.dto';
@Injectable()
export class TodosService {
constructor(@InjectModel(Todo.name) private todoModel: Model<Todo>) {}
async create(createTodoDto: CreateTodoDto): Promise<Todo> {
const createdTodo = new this.todoModel(createTodoDto);
return createdTodo.save();
}
async findAll(): Promise<Todo[]> {
return this.todoModel.find().exec();
}
async findOne(id: string): Promise<Todo> {
const todo = await this.todoModel.findById(id).exec();
if (!todo) {
throw new NotFoundException(`Todo with ID "${id}" not found`);
}
return todo;
}
async update(id: string, updateTodoDto: UpdateTodoDto): Promise<Todo> {
const updatedTodo = await this.todoModel
.findByIdAndUpdate(id, updateTodoDto, { new: true })
.exec();
if (!updatedTodo) {
throw new NotFoundException(`Todo with ID "${id}" not found`);
}
return updatedTodo;
}
async remove(id: string): Promise<Todo> {
const deletedTodo = await this.todoModel.findByIdAndDelete(id).exec();
if (!deletedTodo) {
throw new NotFoundException(`Todo with ID "${id}" not found`);
}
return deletedTodo;
}
}
Wire up the TodosController with route handlers
The controller is the thin layer between HTTP requests and your service. Each method uses a decorator to define the HTTP verb and route path, then delegates the work to the service.
- Open src/todos/todos.controller.ts and replace its contents with this code:
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
import { TodosService } from './todos.service';
import { CreateTodoDto } from './dto/create-todo.dto';
import { UpdateTodoDto } from './dto/update-todo.dto';
@Controller('todos')
export class TodosController {
// NestJS injects the service automatically
constructor(private readonly todosService: TodosService) {}
// POST /todos - create a new todo
@Post()
create(@Body() createTodoDto: CreateTodoDto) {
return this.todosService.create(createTodoDto);
}
// GET /todos - return all todos
@Get()
findAll() {
return this.todosService.findAll();
}
// GET /todos/:id - return a single todo
@Get(':id')
findOne(@Param('id') id: string) {
return this.todosService.findOne(id);
}
// PATCH /todos/:id - update a todo
@Patch(':id')
update(@Param('id') id: string, @Body() updateTodoDto: UpdateTodoDto) {
return this.todosService.update(id, updateTodoDto);
}
// DELETE /todos/:id - remove a todo
@Delete(':id')
remove(@Param('id') id: string) {
return this.todosService.remove(id);
}
}
What does this code do?
Each method in the controller handles one HTTP endpoint:
- @Post() maps to POST /todos. The @Body() decorator extracts the request body and validates it against CreateTodoDto.
- @Get() with no argument maps to GET /todos (all todos). @Get(':id') maps to GET /todos/:id (one todo).
- @Param('id') extracts the :id route parameter and passes it to the service method as a string.
- Each handler simply returns the service call. NestJS automatically serializes the result to JSON.
- Save todos.controller.ts.
- Check your terminal. The dev server should recompile with no errors in the console.
Seeing errors about missing providers?
- Confirm that TodosService is listed in the providers array of todos.module.ts. The nest generate command from the previous step should have added it automatically.
- Make sure you imported CreateTodoDto and UpdateTodoDto from the correct relative paths (./dto/create-todo.dto and ./dto/update-todo.dto).
Why is NestJS saying it cannot resolve dependencies?
✔️ Awesome, I've got everything!
Great. Double check you have saved todos.controller.ts before moving on.
ⓧ I'd like to double check the full code
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
import { TodosService } from './todos.service';
import { CreateTodoDto } from './dto/create-todo.dto';
import { UpdateTodoDto } from './dto/update-todo.dto';
@Controller('todos')
export class TodosController {
constructor(private readonly todosService: TodosService) {}
@Post()
create(@Body() createTodoDto: CreateTodoDto) {
return this.todosService.create(createTodoDto);
}
@Get()
findAll() {
return this.todosService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.todosService.findOne(id);
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateTodoDto: UpdateTodoDto) {
return this.todosService.update(id, updateTodoDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.todosService.remove(id);
}
}
Test the full CRUD flow
Your API is now fully wired. Time to test each endpoint to confirm everything works end to end.
- Open a new terminal window (keep npm run start:dev running in the first one).
Create a new todo by running this command:
curl -X POST http://localhost:3000/todos -H "Content-Type: application/json" -d '{"title":"Learn NestJS","description":"Build a todo API"}'
What should I see?
You should see a JSON response containing your new todo with an _id field, title, description, completed: false, and createdAt/updatedAt timestamps. Copy the _id value. You will need it for the next commands.
- Get all todos by running:
curl http://localhost:3000/todos
- Get a single todo by running this command (replace the ID with the _id from your create response):
curl http://localhost:3000/todos/[[TODO_ID="the _id from your create response"]]
- Update the todo to mark it as completed:
curl -X PATCH http://localhost:3000/todos/[[TODO_ID="the _id from your create response"]] -H "Content-Type: application/json" -d '{"completed":true}'
- Delete the todo:
curl -X DELETE http://localhost:3000/todos/[[TODO_ID="the _id from your create response"]]
What should I see?
Each command returns JSON. GET all returns an array. GET one returns a single object. PATCH returns the updated document with completed: true. DELETE returns the deleted document one final time.
You can also verify in MongoDB Atlas. Click Browse Collections on your cluster to see the todo-app database and todos collection with your documents.
Getting a connection refused error?
- Make sure npm run start:dev is still running in your other terminal window. The server must be active to accept requests.
- If you see a 404, double check the URL path is /todos (plural). The controller uses @Controller('todos') which defines this route prefix.
- If you get a 400 validation error on POST, make sure you included the -H "Content-Type: application/json" header and the JSON body has a title field.
Why are my curl requests to localhost:3000 failing?
Secret mission
Add Filtering and Toggle
Your API can create, read, update, and delete todos. But what if you only want to see completed todos? Or flip a todo's status with a single request? Extend your API with query-based filtering and a dedicated toggle endpoint.
Clean Up Your Resources
Clean Up Your Resources
This project runs on a free MongoDB Atlas M0 cluster, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- MongoDB Atlas free cluster (with todo-app database).
- Local todo-api project folder on your Desktop.
- NestJS CLI installed globally via npm.
Keep everything running
No action needed. Choose this if you're still actively building or plan to extend the API soon.
- Your MongoDB Atlas free cluster stays running at no cost. Atlas will automatically pause it after 30 days of inactivity.
- Your local todo-api folder remains on your Desktop.
- The NestJS CLI stays installed globally on your machine.
Pause - I'll come back to this later
Shut down running processes to free up resources. Your files and cloud data stay intact.
- Stop the dev server by pressing Ctrl+C in your terminal.
- Your Atlas free cluster will automatically pause after 30 days of inactivity. No manual action needed.
- Your local files remain untouched for when you return.
Delete - I don't want to use this again
Remove all project resources and start fresh. This is permanent.
Delete your MongoDB Atlas cluster:
- Log in to your Atlas console at cloud.mongodb.com.
- In the sidebar, click Clusters under the Database heading.
- Click the ... button next to your cluster name.
- Click Terminate from the dropdown menu.
- Enter the cluster name to confirm, then click Terminate.
Delete your local project folder:
macOS
- Delete the project folder by running:
cd ~/Desktop
rm -rf todo-api
Windows
- Delete the project folder by running:
cd ~\Desktop
rmdir /s /q todo-api
Uninstall the NestJS CLI:
- Remove the globally installed CLI by running:
npm uninstall -g @nestjs/cli
Delete your MongoDB Atlas account (optional):
- Click your name in the top-right corner of the Atlas UI.
- Select Manage your MongoDB Account.
- Click Profile Info in the left-hand navigation panel.
- Click Delete Account and follow the on-screen prompts to confirm. This is permanent and cannot be undone.
Nice Work!
Nice Work!
Nice work! You just built a production-style REST API from scratch using NestJS and MongoDB Atlas.
You've learned how to:
- Architect a NestJS application with proper module boundaries, connecting to a cloud MongoDB Atlas database using Mongoose.
- Define a schema with NestJS decorators and enforce type-safe input using DTOs and class-validator.
- Implement full CRUD operations with proper error handling using NotFoundException, and test all five endpoints with curl.
- Secret Mission: Added query-based filtering by completion status and a toggle-complete endpoint.
Ready to quiz yourself?