Build a Bookmark Manager REST API
Build a full CRUD REST API with NestJS using modules, services, and DTOs.
Introduction
30 Second Summary
Think about how many links you bookmark every day. Without a system to organize them, they pile up into a mess of forgotten tabs and lost articles.
In this project, you will build a Bookmark Manager REST API using NestJS. You'll create endpoints that let you create, read, update, and delete bookmarks with automatic request validation.
What You'll Build
A REST API built with NestJS that exposes five endpoints for managing bookmarks, complete with request validation and proper error handling.
By the end of this project, you'll have:
- A NestJS project structured with modules, controllers, and services demonstrating dependency injection.
- Five REST endpoints (GET all, GET one, POST, PATCH, DELETE) for full CRUD bookmark management.
- DTOs with class-validator decorators that automatically reject malformed requests through a global ValidationPipe.
- Secret Mission: Add search and filter functionality so you can query bookmarks by tag or title substring.
Are there any prerequisites?
Basic TypeScript familiarity is helpful for this project. You'll also need Node.js v20 or higher installed on your machine.
Set Up Your NestJS Project
Every REST API needs a solid foundation before you start building endpoints. NestJS provides that foundation through its CLI, which scaffolds a project with a clear module-based architecture.
In this step, you will install the NestJS CLI, scaffold a new project, add validation packages, and generate the files for your bookmarks feature.
In this step, get ready to:
- Install the NestJS CLI and scaffold the project.
- Install validation dependencies and configure a global ValidationPipe.
- Generate the BookmarksModule, BookmarksController, and BookmarksService.
Install the NestJS CLI and scaffold your project
The NestJS CLI is a command-line tool that scaffolds projects and generates components like modules, controllers, and services.
- Open your terminal. 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
Seeing a permissions error?
On macOS or Linux, you may need to prefix the command with sudo. On Windows, try running your terminal as Administrator.
help me fix a permissions error when installing the NestJS CLI globally with npm
- Scaffold your new project by running this command:
nest new bookmark-api
What will this command ask me?
The CLI will prompt you to select a package manager. Use your arrow keys to highlight npm and press Enter.
- Wait for the installation to finish. The CLI creates a bookmark-api folder, installs all dependencies, and initializes a Git repository.
- Navigate into your new project and start the development server by running these commands:
cd bookmark-api
npm run start
- Open your browser and navigate to http://localhost:3000/.
- You should see Hello World! displayed in the browser.
What does this confirm?
Seeing Hello World! means your NestJS project scaffolded correctly, all dependencies installed, and the app compiles and serves on port 3000.
Not seeing Hello World?
Make sure you selected npm during the scaffold prompt. If you chose a different package manager, delete the bookmark-api folder and run nest new bookmark-api again.
Check that no other process is using port 3000. If you see EADDRINUSE, close the other process or stop it before retrying.
help me troubleshoot why my NestJS app isn't showing Hello World at localhost:3000
- Return to your terminal and press Ctrl+C to stop the development server.
Install validation dependencies
NestJS can automatically validate incoming request data using decorators. This requires two packages that power the validation system, plus a utility for creating update DTOs.
- Install the validation packages by running this command:
npm i --save class-validator class-transformer @nestjs/mapped-types
What are these packages?
class-validator provides decorators like @IsUrl() and @IsString() to define validation rules on class properties.
class-transformer converts plain JSON request bodies into class instances so those decorators can run.
@nestjs/mapped-types provides utilities like PartialType for creating DTO variants without duplicating code.
Install command failed?
Make sure you are inside the bookmark-api folder. Run pwd to check your current directory. It should end with /bookmark-api.
help me fix an npm install error inside my NestJS project
Now you need to tell NestJS to use these validation packages on every incoming request. You do this by enabling a global ValidationPipe in your app's entry file.
- In VS Code, open the bookmark-api folder. Click File in the top menu bar.
- Click Open Folder.
- Select the bookmark-api folder on your Desktop, then click Open.
- In the Explorer sidebar, expand the src folder and click main.ts to open it.
- Replace the entire contents of src/main.ts with the following 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);
// Enable validation on all incoming requests
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}));
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
What does this code do?
whitelist: true strips any properties from the request body that don't have a validation decorator. This prevents unexpected fields from reaching your service.
forbidNonWhitelisted: true goes further by throwing an error if someone sends an extra field. Instead of silently ignoring it, the API rejects the request.
transform: true automatically converts plain JSON objects into typed DTO class instances so your validation decorators can run.
- Save src/main.ts by pressing Cmd+S (macOS) or Ctrl+S (Windows).
Generate the bookmarks module, controller, and service
NestJS organizes code into modules. Each module groups related controllers (which handle HTTP requests) and services (which contain business logic). The CLI can generate all three for you.
- Open a terminal inside VS Code by pressing Ctrl+` (backtick key, top-left of keyboard).
- Generate the bookmarks module, controller, and service by running these commands one at a time:
nest generate module bookmarks
nest generate controller bookmarks
nest generate service bookmarks
What do these commands create?
nest generate module bookmarks creates src/bookmarks/bookmarks.module.ts and automatically registers it in app.module.ts.
nest generate controller bookmarks creates src/bookmarks/bookmarks.controller.ts and registers it in bookmarks.module.ts.
nest generate service bookmarks creates src/bookmarks/bookmarks.service.ts and registers it in bookmarks.module.ts.
Why modules, controllers, and services?
This three-part pattern is how NestJS implements Dependency Injection. Controllers receive HTTP requests but don't contain business logic. Services contain the logic and are injected into controllers. Modules tie them together and tell NestJS what belongs where.
This separation makes your code testable and maintainable as it grows.
The CLI also generated spec (test) files alongside each component. Those are fine to leave in place.
Now you need to clean up app.module.ts. The scaffold created default AppController and AppService references that you no longer need since all your logic will live in the bookmarks module.
- In VS Code, open src/app.module.ts from the Explorer sidebar.
- Replace the entire contents of the file with the following code:
import { Module } from '@nestjs/common';
import { BookmarksModule } from './bookmarks/bookmarks.module';
@Module({
imports: [BookmarksModule],
})
export class AppModule {}
What does this code do?
This tells NestJS that the only feature module in your app is BookmarksModule. By removing AppController and AppService, you keep the root module focused solely on importing feature modules.
- Save src/app.module.ts.
- Verify the app still compiles by running this command in the VS Code terminal:
npm run start
- You should see output ending with a line like Nest application successfully started. The server is running, confirming all modules are wired correctly.
- Press Ctrl+C to stop the server.
Seeing a compilation error?
If you see an error about AppController or AppService, double-check that you removed all references to them in app.module.ts. Your file should only import Module from @nestjs/common and BookmarksModule.
help me fix a compilation error in my NestJS app after cleaning up app.module.ts
✔️ Awesome, I've got everything!
Great. Double check you have saved both src/main.ts and src/app.module.ts.
ⓧ I'd like to double check the full code
Here is the full code for each file you modified or that was generated in this step.
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({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}));
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
import { Module } from '@nestjs/common';
import { BookmarksModule } from './bookmarks/bookmarks.module';
@Module({
imports: [BookmarksModule],
})
export class AppModule {}
import { Module } from '@nestjs/common';
import { BookmarksController } from './bookmarks.controller';
import { BookmarksService } from './bookmarks.service';
@Module({
controllers: [BookmarksController],
providers: [BookmarksService],
})
export class BookmarksModule {}
import { Controller } from '@nestjs/common';
@Controller('bookmarks')
export class BookmarksController {}
import { Injectable } from '@nestjs/common';
@Injectable()
export class BookmarksService {}
Your project foundation is ready. You have a NestJS app with validation enabled, and a dedicated bookmarks module waiting for its logic. Next up, you will define the data shapes and validation rules for your bookmarks.
Define Your Bookmark Data and DTOs
Your NestJS project has a module, controller, and service ready to go. Before you can build any endpoints, you need to define what a bookmark actually looks like in code.
In this step, you'll create a TypeScript interface for your bookmark data and two DTOs (Data Transfer Objects) that tell NestJS exactly how to validate incoming requests.
In this step, get ready to:
- Create the Bookmark interface defining your data shape.
- Build a CreateBookmarkDto with validation decorators.
- Set up an UpdateBookmarkDto that makes all fields optional.
Create the Bookmark interface
A TypeScript interface defines the shape of your data without adding any runtime code. Your service and controller will reference this interface so TypeScript can catch type errors before you even run the app.
- In VS Code, create a new folder called interfaces inside src/bookmarks/.
- Inside that folder, create a new file called bookmark.interface.ts and add the following code:
export interface Bookmark {
id: number;
url: string;
title: string;
description?: string;
tags?: string[];
createdAt: Date;
}
What does this code do?
This interface describes every bookmark your API will manage. Each bookmark has a numeric id, a url, a title, and a createdAt timestamp.
The ? after description and tags marks them as optional fields. Not every bookmark needs a description or tags.
- Save bookmark.interface.ts.
- You won't see a change yet. The service will use this interface in the next step to type-check its bookmark storage.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
export interface Bookmark {
id: number;
url: string;
title: string;
description?: string;
tags?: string[];
createdAt: Date;
}
Create the CreateBookmarkDto with validation decorators
A DTO defines what data the client is allowed to send in a request body. By attaching class-validator decorators to each field, NestJS will automatically reject any request that doesn't match your rules.
- Create a new folder called dto inside src/bookmarks/.
- Inside that folder, create a new file called create-bookmark.dto.ts and add the following code:
import { IsUrl, IsString, IsNotEmpty, IsOptional, IsArray } from 'class-validator';
export class CreateBookmarkDto {
@IsUrl()
url: string;
@IsString()
@IsNotEmpty()
title: string;
@IsString()
@IsOptional()
description?: string;
@IsArray()
@IsOptional()
tags?: string[];
}
What does this code do?
Each decorator acts as a validation rule. @IsUrl() ensures the url field is a properly formatted URL. @IsNotEmpty() prevents blank titles from slipping through.
The @IsOptional() decorator on description and tags means those fields can be omitted entirely. If they ARE provided, the other decorators (@IsString(), @IsArray()) still check their type.
- Save create-bookmark.dto.ts.
- You won't see a change yet. The controller will use this DTO when you wire up the POST endpoint.
Seeing red squiggly lines in VS Code?
Make sure you installed class-validator in Step 1. If the import line shows an error, run npm i --save class-validator class-transformer from your bookmark-api folder.
help me fix class-validator import errors in my NestJS project
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
import { IsUrl, IsString, IsNotEmpty, IsOptional, IsArray } from 'class-validator';
export class CreateBookmarkDto {
@IsUrl()
url: string;
@IsString()
@IsNotEmpty()
title: string;
@IsString()
@IsOptional()
description?: string;
@IsArray()
@IsOptional()
tags?: string[];
}
Create the UpdateBookmarkDto with PartialType
When updating a bookmark, the client should only need to send the fields they want to change. Instead of writing a second DTO from scratch, NestJS provides a utility called PartialType that automatically makes every field from your CreateBookmarkDto optional.
- Install the @nestjs/mapped-types package (which provides PartialType) by running this command in your terminal:
npm i --save @nestjs/mapped-types
Why a separate package?
The NestJS CLI scaffold doesn't include @nestjs/mapped-types by default. This small package provides utilities like PartialType, PickType, and OmitType that help you derive new DTOs from existing ones without duplicating code.
- Inside the dto folder, create a new file called update-bookmark.dto.ts and add the following code:
import { PartialType } from '@nestjs/mapped-types';
import { CreateBookmarkDto } from './create-bookmark.dto';
export class UpdateBookmarkDto extends PartialType(CreateBookmarkDto) {}
What does this code do?
PartialType takes your CreateBookmarkDto and creates a new class where every field is optional. This means a PATCH request can send just a new title without needing to also include url or tags.
All the original validation decorators still apply to fields that ARE provided. If someone sends an invalid URL in an update, it still gets rejected.
- Save update-bookmark.dto.ts.
- Verify everything compiles by running this command in your terminal:
npm run start
You should see the app start without errors and display a message like Nest application successfully started. Press Ctrl+C to stop the server.
Seeing a compilation error?
Check that your file names match exactly: bookmark.interface.ts, create-bookmark.dto.ts, and update-bookmark.dto.ts. TypeScript imports are case-sensitive.
If you see an error about @nestjs/mapped-types, make sure the install command finished successfully. Run npm i --save @nestjs/mapped-types again from your bookmark-api folder.
help me fix a TypeScript compilation error in my NestJS bookmark DTOs
✔️ Awesome, I've got everything!
Great. Double check you've saved all three files before moving on.
ⓧ I'd like to double check the full code
export interface Bookmark {
id: number;
url: string;
title: string;
description?: string;
tags?: string[];
createdAt: Date;
}
import { IsUrl, IsString, IsNotEmpty, IsOptional, IsArray } from 'class-validator';
export class CreateBookmarkDto {
@IsUrl()
url: string;
@IsString()
@IsNotEmpty()
title: string;
@IsString()
@IsOptional()
description?: string;
@IsArray()
@IsOptional()
tags?: string[];
}
import { PartialType } from '@nestjs/mapped-types';
import { CreateBookmarkDto } from './create-bookmark.dto';
export class UpdateBookmarkDto extends PartialType(CreateBookmarkDto) {}
Your data layer is locked in. Next up, you'll build the BookmarksService that uses this interface to store bookmarks and implements the core CRUD logic.
Build the Bookmarks Service
Your module structure is in place and your DTOs define what valid bookmark data looks like. Now you need the business logic that actually stores and manages bookmarks.
The service layer in NestJS is where all your data operations live. In this step, you'll build the BookmarksService with methods to create, read, update, delete, and search bookmarks from an in-memory array.
In this step, get ready to:
- Set up in-memory storage and read methods in the service.
- Implement create, update, remove, and search operations.
- Verify the service compiles without errors.
Set up in-memory storage and read methods
Every service needs somewhere to hold data. For this project, a simple array acts as your bookmark store, and a counter variable handles auto-incrementing IDs.
- Open src/bookmarks/bookmarks.service.ts and replace its entire contents with this code:
import { Injectable, NotFoundException } from '@nestjs/common';
import { Bookmark } from './interfaces/bookmark.interface';
import { CreateBookmarkDto } from './dto/create-bookmark.dto';
import { UpdateBookmarkDto } from './dto/update-bookmark.dto';
@Injectable()
export class BookmarksService {
// In-memory array to store bookmarks
private bookmarks: Bookmark[] = [];
// Auto-incrementing counter for unique IDs
private idCounter = 1;
// Return all bookmarks in the store
findAll(): Bookmark[] {
return this.bookmarks;
}
// Find a single bookmark by ID, or throw a 404
findOne(id: number): Bookmark {
const bookmark = this.bookmarks.find((b) => b.id === id);
if (!bookmark) {
throw new NotFoundException(`Bookmark with ID ${id} not found`);
}
return bookmark;
}
}
What does this code do?
The bookmarks array stores all bookmark objects in memory. The idCounter starts at 1 and increments every time a new bookmark is created, giving each one a unique ID.
The findAll() method returns the entire array. The findOne() method searches for a specific ID and throws a NotFoundException if nothing matches. NestJS automatically converts that exception into a proper 404 HTTP response.
You won't see a change yet. The next chunk adds the write methods that complete the service.
Implement the write and search methods
With the read operations ready, you need methods to create new bookmarks, update existing ones, remove them, and search through them by tag or title.
- In src/bookmarks/bookmarks.service.ts, add these methods inside the BookmarksService class, below the closing brace of findOne():
// Create a new bookmark with auto-generated id and timestamp
create(createBookmarkDto: CreateBookmarkDto): Bookmark {
const bookmark: Bookmark = {
id: this.idCounter++,
...createBookmarkDto,
createdAt: new Date(),
};
this.bookmarks.push(bookmark);
return bookmark;
}
// Update an existing bookmark by ID, or throw a 404
update(id: number, updateBookmarkDto: UpdateBookmarkDto): Bookmark {
const index = this.bookmarks.findIndex((b) => b.id === id);
if (index === -1) {
throw new NotFoundException(`Bookmark with ID ${id} not found`);
}
this.bookmarks[index] = {
...this.bookmarks[index],
...updateBookmarkDto,
};
return this.bookmarks[index];
}
// Remove a bookmark by ID, or throw a 404
remove(id: number): void {
const index = this.bookmarks.findIndex((b) => b.id === id);
if (index === -1) {
throw new NotFoundException(`Bookmark with ID ${id} not found`);
}
this.bookmarks.splice(index, 1);
}
// Filter bookmarks by exact tag match or case-insensitive title substring
search(tag?: string, title?: string): Bookmark[] {
let results = this.bookmarks;
if (tag) {
results = results.filter((b) => b.tags?.includes(tag));
}
if (title) {
const lowerTitle = title.toLowerCase();
results = results.filter((b) => b.title.toLowerCase().includes(lowerTitle));
}
return results;
}
What does this code do?
The create() method spreads the DTO properties into a new bookmark object, assigns an auto-incrementing id, and stamps a createdAt date. It pushes the bookmark into the array and returns it.
The update() and remove() methods both use findIndex() to locate the bookmark. If the index is -1 (not found), they throw a NotFoundException that NestJS converts into a 404 response.
The search() method filters the full bookmark list. It matches tags exactly and titles as case-insensitive substrings. Both parameters are optional, so you can search by either or both.
- Save src/bookmarks/bookmarks.service.ts.
Verify the service compiles
The service methods don't produce visible output on their own because nothing calls them yet. Running the app confirms TypeScript is happy with your code.
- Start the application by running this command in your terminal (make sure you're inside the bookmark-api folder):
npm run start
- You should see a log ending with Nest application successfully started in your terminal. This confirms your service compiles and the module wiring is correct.
- Press Ctrl+C to stop the server.
Seeing a compilation error?
- Check that the closing brace of findOne() is followed by the new methods, and that the entire class ends with a final }. A missing brace is the most common cause.
- Verify that your imports at the top include NotFoundException from @nestjs/common.
- Make sure the Bookmark import path is ./interfaces/bookmark.interface (not a typo like interface without the 's').
Help me debug my NestJS service compilation error
✔️ Awesome, I've got everything!
Great. Double check you've saved bookmarks.service.ts and move on.
ⓧ I'd like to double check the full code
import { Injectable, NotFoundException } from '@nestjs/common';
import { Bookmark } from './interfaces/bookmark.interface';
import { CreateBookmarkDto } from './dto/create-bookmark.dto';
import { UpdateBookmarkDto } from './dto/update-bookmark.dto';
@Injectable()
export class BookmarksService {
private bookmarks: Bookmark[] = [];
private idCounter = 1;
findAll(): Bookmark[] {
return this.bookmarks;
}
findOne(id: number): Bookmark {
const bookmark = this.bookmarks.find((b) => b.id === id);
if (!bookmark) {
throw new NotFoundException(`Bookmark with ID ${id} not found`);
}
return bookmark;
}
create(createBookmarkDto: CreateBookmarkDto): Bookmark {
const bookmark: Bookmark = {
id: this.idCounter++,
...createBookmarkDto,
createdAt: new Date(),
};
this.bookmarks.push(bookmark);
return bookmark;
}
update(id: number, updateBookmarkDto: UpdateBookmarkDto): Bookmark {
const index = this.bookmarks.findIndex((b) => b.id === id);
if (index === -1) {
throw new NotFoundException(`Bookmark with ID ${id} not found`);
}
this.bookmarks[index] = {
...this.bookmarks[index],
...updateBookmarkDto,
};
return this.bookmarks[index];
}
remove(id: number): void {
const index = this.bookmarks.findIndex((b) => b.id === id);
if (index === -1) {
throw new NotFoundException(`Bookmark with ID ${id} not found`);
}
this.bookmarks.splice(index, 1);
}
search(tag?: string, title?: string): Bookmark[] {
let results = this.bookmarks;
if (tag) {
results = results.filter((b) => b.tags?.includes(tag));
}
if (title) {
const lowerTitle = title.toLowerCase();
results = results.filter((b) => b.title.toLowerCase().includes(lowerTitle));
}
return results;
}
}
Why does NotFoundException matter?
NestJS provides built-in exception classes like NotFoundException that map directly to HTTP status codes. When your service throws a NotFoundException, NestJS automatically sends a 404 response with a descriptive error message.
This means your service layer handles errors cleanly without needing manual HTTP response logic. The controller doesn't need try/catch blocks.
Your service layer is complete with full CRUD operations and search functionality. Next up, you'll wire these methods to HTTP endpoints by building out the controller.
Wire Up the Controller Endpoints
Your BookmarksService has all the business logic for managing bookmarks. But right now, no HTTP request can reach it.
The controller is the HTTP gateway that sits between incoming requests and the service layer. It receives requests, delegates the work to the service, and returns responses. This is where dependency injection connects everything together.
In this step, get ready to:
- Inject BookmarksService into the controller via constructor injection.
- Implement REST endpoints for creating, reading, updating, and deleting bookmarks.
- Test the API with curl commands to verify everything works end-to-end.
Set up imports and inject BookmarksService
NestJS uses constructor injection to provide services to controllers. You declare the service as a constructor parameter, and the framework handles creating and passing the instance automatically.
- In VS Code, open src/bookmarks/bookmarks.controller.ts.
- Replace the entire file contents with the following code:
import {
Controller,
Get,
Post,
Patch,
Delete,
Body,
Param,
Query,
} from '@nestjs/common';
import { BookmarksService } from './bookmarks.service';
import { CreateBookmarkDto } from './dto/create-bookmark.dto';
import { UpdateBookmarkDto } from './dto/update-bookmark.dto';
@Controller('bookmarks')
export class BookmarksController {
// NestJS automatically injects an instance of BookmarksService
constructor(private readonly bookmarksService: BookmarksService) {}
}
What does this code do?
The import block pulls in all the HTTP method decorators you will use (Get, Post, Patch, Delete) plus parameter decorators (Body, Param, Query) from @nestjs/common.
The constructor(private readonly bookmarksService: BookmarksService) line is where dependency injection happens. NestJS sees that BookmarksService is registered as a provider in BookmarksModule, creates an instance, and passes it in automatically.
- Save bookmarks.controller.ts.
The controller compiles fine with no route handlers yet. You will add endpoints in the next chunk.
Implement the REST endpoints
Each endpoint is a method decorated with an HTTP method decorator. The decorator tells NestJS which HTTP verb and route path to match. Parameter decorators like @Body() and @Param() extract data from the incoming request.
- In bookmarks.controller.ts, find the closing brace of the constructor:
constructor(private readonly bookmarksService: BookmarksService) {}
- Add the following endpoint methods inside the class, directly after the constructor:
// Search bookmarks by tag or title substring
// This route MUST come before @Get(':id') to avoid 'search' matching as an id
@Get('search')
search(@Query('tag') tag?: string, @Query('title') title?: string) {
return this.bookmarksService.search(tag, title);
}
// Return all bookmarks
@Get()
findAll() {
return this.bookmarksService.findAll();
}
// Return a single bookmark by its numeric ID
@Get(':id')
findOne(@Param('id') id: string) {
return this.bookmarksService.findOne(+id);
}
// Create a new bookmark (validated by CreateBookmarkDto)
@Post()
create(@Body() createBookmarkDto: CreateBookmarkDto) {
return this.bookmarksService.create(createBookmarkDto);
}
// Update an existing bookmark (validated by UpdateBookmarkDto)
@Patch(':id')
update(@Param('id') id: string, @Body() updateBookmarkDto: UpdateBookmarkDto) {
return this.bookmarksService.update(+id, updateBookmarkDto);
}
// Delete a bookmark by ID
@Delete(':id')
remove(@Param('id') id: string) {
return this.bookmarksService.remove(+id);
}
What does this code do?
- @Get('search') is defined before @Get(':id') because NestJS matches routes top-to-bottom. If :id came first, the word "search" would be treated as an ID.
- @Query('tag') and @Query('title') extract query string parameters like ?tag=dev&title=nest from the URL.
- +id converts the string route parameter to a number, since your service expects a numeric ID.
- @Body() extracts the JSON request body and automatically validates it against the DTO class (thanks to the global ValidationPipe you set up earlier).
- Save bookmarks.controller.ts.
Seeing red squiggly lines?
Make sure every method is inside the BookmarksController class (between the opening { after the class declaration and the final }).
Check that your import block at the top includes Query in the list from @nestjs/common. A missing import causes TypeScript errors on the decorator.
help me fix TypeScript errors in my NestJS controller
✔️ Awesome, I've got everything!
Great. Make sure you have saved bookmarks.controller.ts before continuing.
ⓧ I'd like to double check the full code
import {
Controller,
Get,
Post,
Patch,
Delete,
Body,
Param,
Query,
} from '@nestjs/common';
import { BookmarksService } from './bookmarks.service';
import { CreateBookmarkDto } from './dto/create-bookmark.dto';
import { UpdateBookmarkDto } from './dto/update-bookmark.dto';
@Controller('bookmarks')
export class BookmarksController {
constructor(private readonly bookmarksService: BookmarksService) {}
@Get('search')
search(@Query('tag') tag?: string, @Query('title') title?: string) {
return this.bookmarksService.search(tag, title);
}
@Get()
findAll() {
return this.bookmarksService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.bookmarksService.findOne(+id);
}
@Post()
create(@Body() createBookmarkDto: CreateBookmarkDto) {
return this.bookmarksService.create(createBookmarkDto);
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateBookmarkDto: UpdateBookmarkDto) {
return this.bookmarksService.update(+id, updateBookmarkDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.bookmarksService.remove(+id);
}
}
Test the full API with curl
The controller is wired up. Time to start the server and verify that requests flow through the controller, hit the service, and return proper responses.
- Start the development server by running this command in your terminal (from inside the bookmark-api folder):
npm run start:dev
What does start:dev do?
This runs your NestJS app in watch mode. Every time you save a file, the server automatically recompiles and restarts. You will see a log message confirming the app is listening on port 3000.
- Open a second terminal window (keep the server running in the first one).
- Create a bookmark by running this curl command:
curl -X POST http://localhost:3000/bookmarks -H "Content-Type: application/json" -d '{"url": "https://nestjs.com", "title": "NestJS Docs", "tags": ["dev", "typescript"]}'
You should see a JSON response with an id, url, title, tags, and createdAt fields.
- Verify that validation rejects invalid URLs by running:
curl -X POST http://localhost:3000/bookmarks -H "Content-Type: application/json" -d '{"url": "not-a-url", "title": "Bad Bookmark"}'
You should see a 400 error response with a message array explaining that url must be a URL.
- Retrieve all bookmarks by running:
curl http://localhost:3000/bookmarks
You should see a JSON array containing the NestJS Docs bookmark you created earlier.
- Update the bookmark title by running:
curl -X PATCH http://localhost:3000/bookmarks/1 -H "Content-Type: application/json" -d '{"title": "NestJS Official Docs"}'
The response shows the bookmark with the updated title.
- Delete the bookmark and confirm it is gone by running:
curl -X DELETE http://localhost:3000/bookmarks/1
- Verify the deletion by fetching all bookmarks again:
curl http://localhost:3000/bookmarks
You should see an empty array [], confirming the bookmark was removed.
Getting connection refused?
Make sure your server is still running in the first terminal window. You should see log output indicating it started successfully on port 3000.
If you see a compilation error instead of the server starting, check bookmarks.controller.ts for typos in decorator names or missing closing braces.
help me debug my NestJS server not starting
Your REST API is fully operational. Every endpoint connects to the service, validation rejects bad data, and proper error responses come back for missing resources. Next up, you will extend this API with search and filter functionality in the Secret Mission.
Secret mission
Add Search and Filter
Your API can create, read, update, and delete bookmarks. But what happens when you have dozens of them? In this secret mission, put your search endpoint to the test by seeding data and verifying that tag and title filters return exactly what you expect.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your local machine, so there are no ongoing costs. Decide whether to keep your resources for future reference, pause the running server, or delete everything entirely.
Resources you used:
- The bookmark-api project folder on your Desktop.
- The globally installed @nestjs/cli package.
- The development server running at http://localhost:3000/.
Keep everything running
No action needed. Choose this if you are still actively building on your bookmark API or plan to extend it with a database layer.
- Your dev server continues running at http://localhost:3000/.
- The bookmark-api folder stays on your Desktop for future reference.
- The @nestjs/cli remains globally installed so you can scaffold new NestJS projects anytime.
Pause - I'll come back to this later
Stop the running server to free up your terminal, but keep all project files intact.
- Switch to the terminal where your dev server is running.
- Press Ctrl+C to stop the server.
- Your bookmark-api folder stays on your Desktop. When you are ready to resume, open the folder and run npm run start again.
Delete - I don't want to use this again
Remove all project files and the global CLI tool from your machine.
- Switch to the terminal where your dev server is running and press Ctrl+C to stop it.
- Delete the project folder by running:
macOS
cd ~/Desktop
rm -rf bookmark-api
Windows
cd ~\Desktop
rmdir /s /q bookmark-api
- Uninstall the NestJS CLI by running:
npm uninstall -g @nestjs/cli
- Confirm the CLI is removed by running:
nest --version
You should see a "command not found" message, confirming the CLI is gone.
Nice Work!
Nice Work!
You just built a fully functional REST API with NestJS and TypeScript. Your Bookmark Manager API validates incoming requests, returns proper HTTP error codes, and follows the same architectural patterns used in production backend systems.
You've learned how to:
- Structure a NestJS application using modules, controllers, and services wired together with dependency injection.
- Create DTOs with class-validator decorators and a global ValidationPipe to automatically reject malformed requests.
- Implement full CRUD operations with proper error handling using NotFoundException for missing resources.
- Secret Mission: Added search and filter endpoints using @Query() parameters to find bookmarks by tag or title.
Ready to quiz yourself?