
NestJS API Development Guide 05: Controllers, GET Requests, Params, Queries and Pipes
Video
Watch the full video on YouTube:
Introduction
In this fifth installment of the NestJS guide, we dive deep into the fundamental concepts of Controllers (@Controller), focusing on handling GET requests, routing, and extracting data from URLs. Controllers are the entry point to your NestJS application — they are responsible for receiving HTTP requests and returning responses to the client.
What is a Controller?
A Controller in NestJS is a class decorated with @Controller() that handles incoming HTTP requests. Each controller method is mapped to a specific route and HTTP method, making it easy to organize your API endpoints by resource.
When you create a controller, you typically pass a string to the @Controller() decorator. This string becomes the route prefix for all endpoints defined within that controller. For example, if you define @Controller('pets'), all routes inside that controller will start with /pets.
Cleaning Up the Boilerplate
When you scaffold a new NestJS project, the CLI generates a default AppController with some sample routes. Before building your own controllers, it’s a good practice to clean up the boilerplate — remove the default @Get() handler and the corresponding service method so you start with a clean slate.
@Controller('pets')
export class PetsController {
// Your custom routes go here
}
Route Prefixes
The string you pass to @Controller() acts as a prefix for every route inside that controller. This is how NestJS organizes your API by resource:
@Controller('pets')
export class PetsController {
@Get()
findAll() {
// This responds to GET /pets
}
@Get('featured')
findFeatured() {
// This responds to GET /pets/featured
}
}
This convention keeps your routes clean and predictable. If you later decide to version your API, you can simply change the prefix to v2/pets and all routes update accordingly.
Handling GET Requests
The @Get() decorator maps a method to an HTTP GET endpoint. When you return a value from the handler, NestJS automatically serializes it and sends it back to the client.
Returning Data
NestJS determines the response type based on what you return:
@Get()
findAll() {
return ['cat', 'dog', 'bird']; // Automatically returned as JSON
}
When you return an array or an object, NestJS sets the Content-Type header to application/json and serializes the data. If you return a string, NestJS sends it as plain text with text/html content type.
Serializing Responses
The automatic serialization is one of the conveniences NestJS provides. You don’t need to manually call JSON.stringify() or set headers — just return the data and NestJS handles the rest:
@Get()
findAll() {
return ['cat', 'dog', 'bird']; // Returns JSON array with Content-Type: application/json
}
@Get(' greeting')
greeting() {
return 'Hello, World!'; // Returns plain text with Content-Type: text/html
}
Multiple GET Endpoints
A single controller can have multiple GET handlers, each mapped to a different sub-route. This is useful when you need different endpoints for different kinds of queries on the same resource:
@Get()
findAll() {
return ['cat', 'dog', 'bird']; // GET /pets
}
@Get('featured')
findFeatured() {
return ['cat', 'dog']; // GET /pets/featured
}
@Get('available')
findAvailable() {
return ['bird']; // GET /pets/available
}
Each @Get('subroute') creates a unique endpoint. The string you pass is appended to the controller’s prefix, so @Get('featured') inside @Controller('pets') becomes /pets/featured.
Testing with REST Clients
Before moving on to more advanced routing, it’s important to have a way to test your endpoints. There are several REST client tools available for testing HTTP APIs:
- Kulala — A lightweight Vim/Neovim REST client
- Postman — One of the most popular API development tools
- Insomnia — A clean, open-source REST client
- RapidAPI — A platform for discovering and connecting to APIs
- Thunder Client — A VS Code extension for REST API testing
You can use any of these tools to send GET requests to your NestJS endpoints and inspect the responses. This is essential during development to verify that your controllers are working as expected.
To run your NestJS server in development mode, use:
pnpm run start:dev
This starts the server with hot-reload, so any changes you make are automatically reflected without restarting the server.
Path Parameters
Path parameters (also called route parameters) allow you to capture dynamic segments from the URL. In NestJS, you define path parameters by prefixing a variable name with a colon (:) in the route string, and then extract them using the @Param() decorator.
Single Path Parameter
@Get(':id')
findOne(@Param('id') id: string) {
return `Pet ID: ${id}`;
}
When a request comes in to /pets/123, the id parameter captures the value 123. The @Param('id') decorator tells NestJS to extract the :id segment from the URL and pass it as the id argument to your method.
Multiple Path Parameters
You can define multiple path parameters in a single route. Each colon-prefixed segment becomes a named parameter:
@Get(':id/:type')
findOne(@Param('id') id: string, @Param('type') type: string) {
return `Pet ID: ${id}, Type: ${type}`;
}
A request to /pets/42/cats would result in id = '42' and type = 'cats'. The order of the parameters in the @Param() decorators must match the order of the segments in the route.
Path Parameters with Validation
By default, path parameters are always strings. If you need a specific type (like a number), you can use a Pipe to validate and transform the value. The built-in ParseIntPipe converts a string parameter to an integer and throws an error if the conversion fails:
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return `Pet ID: ${id}`;
}
If someone requests /pets/abc, the ParseIntPipe will throw a BadRequestException because 'abc' cannot be converted to a number. This gives you automatic input validation at the route level.
Combining Path Parameters with Pipes
You can mix validated and non-validated parameters in the same route:
@Get(':id/:type')
findOne(
@Param('id', ParseIntPipe) id: number,
@Param('type') type: string
) {
return `Pet ID: ${id}, Type: ${type}`;
}
Here, id is validated and transformed to a number, while type remains a string. This selective validation lets you enforce type safety where it matters most.
Query Parameters
Query parameters appear after the ? in a URL and are commonly used for filtering, pagination, and sorting. In NestJS, you extract them using the @Query() decorator.
Extracting Individual Query Parameters
You can extract specific query parameters by name:
@Get()
findAll(@Query('limit') limit: number, @Query('offset') offset: number) {
return `Limit: ${limit}, Offset: ${offset}`;
}
A request to /pets?limit=10&offset=20 would give you limit = '10' and offset = '20'. Note that query parameters are always strings — you’ll need to convert them manually or use pipes if you need numbers.
Extracting All Query Parameters as an Object
Instead of extracting individual parameters, you can grab the entire query string as an object:
@Get()
findAll(@Query() query: { limit?: number; offset?: number }) {
return query;
}
This is useful when you have many optional query parameters and don’t want to list each one individually. The query object will contain all the key-value pairs from the URL.
Making Pipes Optional in Queries
One powerful pattern is making validation pipes optional for query parameters. This allows you to apply validation only when a parameter is provided, while leaving it undefined otherwise:
@Get()
findAll(
@Query('limit', new ParseIntPipe({ optional: true })) limit?: number,
@Query('offset', new ParseIntPipe({ optional: true })) offset?: number
) {
return `Limit: ${limit}, Offset: ${offset}`;
}
The { optional: true } configuration tells the pipe to skip validation if the parameter is not present in the query string. This is perfect for optional pagination — you can request /pets, /pets?limit=10, or /pets?limit=10&offset=20 and all work correctly.
Dynamic Filtering with Query Parameters
Query parameters enable powerful filtering patterns. You can build endpoints that dynamically filter data based on what the client sends:
@Get()
findAll(
@Query('species') species?: string,
@Query('minAge') minAge?: number,
@Query('maxAge') maxAge?: number
) {
// Filter pets based on provided query parameters
let results = this.petsService.findAll();
if (species) {
results = results.filter(pet => pet.species === species);
}
if (minAge) {
results = results.filter(pet => pet.age >= minAge);
}
if (maxAge) {
results = results.filter(pet => pet.age <= maxAge);
}
return results;
}
This approach gives clients flexible filtering capabilities while keeping your controller logic clean.
Route Matching and Multiple Parameters
When you have multiple endpoints with different route patterns, NestJS matches incoming requests to the most specific route. Understanding how route matching works is crucial to avoid unexpected behavior.
Static vs Dynamic Routes
Static routes take precedence over dynamic ones. If you define both a static and a dynamic route, NestJS will try the static route first:
@Get('featured')
findFeatured() {
return ['cat', 'dog']; // GET /pets/featured
}
@Get(':id')
findOne(@Param('id') id: string) {
return `Pet ID: ${id}`; // GET /pets/:id
}
A request to /pets/featured will hit the findFeatured handler, not the findOne handler, because featured is a static match.
Ordering Matters
The order in which you define your routes can affect which handler gets called. More specific routes (static paths) should generally be defined before more general ones (dynamic parameters):
@Get()
findAll() { /* GET /pets */ }
@Get('featured')
findFeatured() { /* GET /pets/featured */ }
@Get(':id')
findOne(@Param('id') id: string) { /* GET /pets/:id */ }
With this ordering, /pets/featured correctly routes to findFeatured, while /pets/123 routes to findOne.
Best Practices
- Keep controllers focused — Each controller should handle a single resource (e.g., pets, users, products).
- Use meaningful route names — Choose clear, descriptive sub-routes like
featured,available, orsearchinstead of generic names. - Validate input with pipes — Use
ParseIntPipeand other built-in pipes to ensure path and query parameters are the correct type. - Make pipes optional when needed — Use
{ optional: true }for query parameters that aren’t required. - Order routes carefully — Define static routes before dynamic ones to avoid routing conflicts.
- Test with REST clients — Use tools like Postman, Insomnia, or Thunder Client to verify your endpoints during development.
Conclusion
Controllers are the backbone of any NestJS application. They define how your API is structured, how requests are routed, and how data is extracted from URLs. In this episode, we covered the essentials: creating controllers with route prefixes, handling GET requests, extracting path and query parameters, validating data with pipes, and understanding how route matching works. These concepts form the foundation for building well-organized, type-safe APIs.
Repository
💻 Repository: https://github.com/radagva/guia-de-nestjs-example
Next Steps
🔔 Subscribe to the channel to not miss the next chapters of the NestJS guide!
