Controllers
Controllers are the foundation of your HazelJS application. They handle incoming HTTP requests, process them, and return responses to clients. Controllers use decorators to define routes, extract request data, and configure responses.
Purpose
Controllers serve as the entry point for your application's HTTP endpoints. They:
- Handle Routing: Define which URLs map to which handler methods
- Extract Data: Get parameters, query strings, request bodies, and headers
- Process Requests: Coordinate with services to handle business logic
- Return Responses: Send data back to clients with appropriate status codes
Architecture
Controllers in HazelJS follow a decorator-based pattern that makes routing and request handling declarative and type-safe:
Basic Controller
A controller is a class decorated with @Controller(). The decorator takes an optional path prefix that applies to all routes in the controller.
import { Controller, Get } from '@hazeljs/core';
// @Controller is a class decorator that marks this class as a controller
// The string 'users' is the route prefix for all methods in this controller
@Controller('users')
export class UsersController {
// @Get is a method decorator that creates a GET route
// Combined with @Controller('users'), this creates: GET /users
@Get()
findAll() {
return 'This action returns all users';
}
}
How it works:
@Controller('users')registers the class as a controller with prefix/users@Get()creates a GET route handler- The method name
findAllis used for logging/debugging (not for routing) - The return value is automatically serialized to JSON
Routing
The @Controller() decorator is required to define a basic controller. The path prefix groups related routes and minimizes repetitive code.
HTTP Method Decorators
HazelJS provides decorators for all HTTP methods:
import { Controller, Get, Post, Put, Delete, Patch } from '@hazeljs/core';
@Controller('users')
export class UsersController {
// GET /users
@Get()
findAll() {
return 'This action returns all users';
}
// GET /users/:id
@Get(':id')
findOne(@Param('id') id: string) {
return `This action returns user #${id}`;
}
// POST /users
@Post()
create(@Body() createUserDto: CreateUserDto) {
return 'This action creates a new user';
}
// PUT /users/:id
@Put(':id')
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return `This action updates user #${id}`;
}
// PATCH /users/:id
@Patch(':id')
partialUpdate(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return `This action partially updates user #${id}`;
}
// DELETE /users/:id
@Delete(':id')
remove(@Param('id') id: string) {
return `This action removes user #${id}`;
}
}
Route Paths
Route paths can be:
- Static:
@Get('profile')→/users/profile - Dynamic:
@Get(':id')→/users/:id(capturesidparameter) - Wildcards:
@Get('*')→ matches any path - Combined:
@Get(':userId/posts/:postId')→/users/:userId/posts/:postId
Important: Static routes should be declared before dynamic routes to prevent parameterized paths from intercepting static routes.
Request Object
Handlers often need access to the client request details. HazelJS provides access to the request object using the @Req() decorator.
@Req Decorator
The @Req() decorator is a parameter decorator that injects the Express Request object into your handler method.
Purpose: Provides full access to the HTTP request object, including headers, cookies, IP address, and other request metadata.
How it works:
- Injects the Express
Requestobject as a method parameter - Type-safe when using
@types/express - Use when you need low-level access to request details
import { Controller, Get, Req } from '@hazeljs/core';
import { Request } from 'express';
@Controller('users')
export class UsersController {
@Get()
// @Req() is a parameter decorator that injects the Express Request object
findAll(@Req() request: Request) {
// Access request properties:
console.log(request.headers); // Request headers
console.log(request.cookies); // Cookies
console.log(request.ip); // Client IP address
console.log(request.method); // HTTP method
console.log(request.url); // Request URL
console.log(request.query); // Query parameters (object)
console.log(request.body); // Request body (if parsed)
return 'This action returns all users';
}
}
Hint: To take advantage of Express typings, install
@types/expresspackage:npm install --save-dev @types/express
Route Parameters
Route parameters are dynamic segments in the URL path. They're extracted using the @Param() decorator.
@Param Decorator
The @Param() decorator is a parameter decorator that extracts route parameters from the URL.
Purpose: Gets dynamic path segments (e.g., /users/:id where id is a parameter).
How it works:
- Reads parameter values from the URL path
- Can extract a single parameter or all parameters
- Automatically converts to the specified type
// Single parameter
@Get(':id')
// @Param('id') extracts the 'id' parameter from the URL
findOne(@Param('id') id: string) {
// For URL: /users/123
// id = '123'
return `This action returns user #${id}`;
}
Multiple Parameters:
@Get(':userId/posts/:postId')
// Extract multiple parameters from the path
findUserPost(
@Param('userId') userId: string, // From :userId segment
@Param('postId') postId: string, // From :postId segment
) {
// For URL: /users/123/posts/456
// userId = '123', postId = '456'
return `User ${userId}, Post ${postId}`;
}
All Parameters:
@Get(':id')
findOne(@Param() params: { id: string }) {
// Get all route parameters as an object
// params = { id: '123' }
return params;
}
Type Conversion:
Parameters are strings by default. Convert them as needed:
@Get(':id')
findOne(@Param('id') id: string) {
const userId = parseInt(id, 10); // Convert to number
// Or use a pipe for automatic conversion
return `User #${userId}`;
}
Query Parameters
Query parameters are key-value pairs in the URL after the ? (e.g., ?page=1&limit=10). Use the @Query() decorator to access them.
@Query Decorator
The @Query() decorator is a parameter decorator that extracts query string parameters.
Purpose: Gets query parameters from the URL (e.g., ?page=1&limit=10).
How it works:
- Reads query parameters from the URL
- Can extract a single parameter or all parameters
- Returns
undefinedif parameter doesn't exist
Single Parameter:
@Get()
// @Query('page') extracts the 'page' query parameter
findAll(@Query('page') page: number, @Query('limit') limit: number) {
// For URL: /users?page=1&limit=10
// page = '1' (string), limit = '10' (string)
// Note: Query params are strings by default
const pageNum = parseInt(page as any, 10);
const limitNum = parseInt(limit as any, 10);
return `Page: ${pageNum}, Limit: ${limitNum}`;
}
All Query Parameters:
@Get()
findAll(@Query() query: any) {
// Get all query parameters as an object
// For URL: /users?page=1&limit=10&search=john
// query = { page: '1', limit: '10', search: 'john' }
console.log(query);
return 'This action returns all users';
}
Optional Parameters:
@Get()
findAll(
@Query('page') page?: number, // Optional parameter
@Query('limit') limit?: number, // Optional parameter
) {
// If ?page=1 is not in URL, page = undefined
const pageNum = page || 1;
const limitNum = limit || 10;
return { page: pageNum, limit: limitNum };
}
Request Body
The request body contains data sent by the client (typically in POST, PUT, PATCH requests). Use the @Body() decorator to access it.
@Body Decorator
The @Body() decorator is a parameter decorator that extracts and validates the request body.
Purpose: Gets the request body data, typically used for creating or updating resources.
How it works:
- Parses JSON request body automatically
- Can validate against a DTO class (with validation pipes)
- Type-safe when using TypeScript interfaces or classes
Basic Usage:
@Post()
// @Body() extracts the request body
create(@Body() createUserDto: CreateUserDto) {
// Request body is automatically parsed as JSON
// TypeScript ensures type safety
return `Creating user: ${createUserDto.name}`;
}
DTO (Data Transfer Object) Classes:
Define a DTO class to structure and validate request data:
// create-user.dto.ts
export class CreateUserDto {
name: string; // Required field
email: string; // Required field
age: number; // Required field
}
// In controller
@Post()
create(@Body() createUserDto: CreateUserDto) {
// createUserDto is typed as CreateUserDto
// Properties are validated if using validation pipes
return this.userService.create(createUserDto);
}
Partial Body:
@Post()
create(@Body('name') name: string) {
// Extract only the 'name' property from body
// For body: { name: 'John', email: 'john@example.com' }
// name = 'John'
return `Creating user: ${name}`;
}
Validation with Pipes:
import { Body, UsePipes, ValidationPipe } from '@hazeljs/core';
import { IsString, IsEmail, MinLength } from 'class-validator';
class CreateUserDto {
@IsString()
@MinLength(3)
name: string;
@IsEmail()
email: string;
}
@Post()
@UsePipes(new ValidationPipe())
create(@Body() createUserDto: CreateUserDto) {
// ValidationPipe automatically validates the DTO
// Throws BadRequestException if validation fails
return this.userService.create(createUserDto);
}
Headers
Custom headers can be accessed using the @Headers() decorator.
@Headers Decorator
The @Headers() decorator is a parameter decorator that extracts HTTP headers from the request.
Purpose: Gets header values from the request, useful for authentication tokens, API keys, etc.
How it works:
- Reads header values (case-insensitive)
- Can extract a single header or all headers
- Returns
undefinedif header doesn't exist
Single Header:
@Get()
// @Headers('authorization') extracts the Authorization header
findAll(@Headers('authorization') auth: string) {
// For header: Authorization: Bearer token123
// auth = 'Bearer token123'
console.log(auth);
return 'This action returns all users';
}
All Headers:
@Get()
findAll(@Headers() headers: Record<string, string>) {
// Get all headers as an object
// headers = { 'authorization': 'Bearer ...', 'content-type': 'application/json', ... }
console.log(headers);
return 'This action returns all users';
}
Common Use Cases:
// Authentication token
@Get('profile')
getProfile(@Headers('authorization') token: string) {
// Extract and validate token
return this.authService.validateToken(token);
}
// API version
@Get()
findAll(@Headers('api-version') version: string) {
// Handle different API versions
if (version === 'v2') {
return this.getV2Data();
}
return this.getV1Data();
}
// Custom headers
@Get()
findAll(@Headers('x-custom-header') custom: string) {
// Access custom headers (prefixed with 'x-')
return { custom };
}
Status Codes
By default, HazelJS sets response status codes automatically:
- 200 OK: For GET, PUT, PATCH, DELETE requests
- 201 Created: For POST requests
Use the @HttpCode() decorator to override the default status code.
@HttpCode Decorator
The @HttpCode() decorator is a method decorator that sets the HTTP status code for the response.
Purpose: Explicitly control the status code returned to the client.
How it works:
- Sets the status code in the response
- Applied at the method level
- Overrides default status codes
import { Controller, Post, HttpCode } from '@hazeljs/core';
@Controller('users')
export class UsersController {
@Post()
@HttpCode(204) // No Content - successful but no response body
create() {
// Returns 204 No Content instead of default 201 Created
return 'This action creates a new user';
}
@Post('custom')
@HttpCode(202) // Accepted - request accepted but not yet processed
createAsync() {
// Returns 202 Accepted
return { message: 'Request accepted' };
}
}
Common Status Codes:
@HttpCode(200) // OK - default for most methods
@HttpCode(201) // Created - default for POST
@HttpCode(204) // No Content - success with no body
@HttpCode(202) // Accepted - async processing
@HttpCode(400) // Bad Request - validation errors
@HttpCode(401) // Unauthorized - authentication required
@HttpCode(403) // Forbidden - insufficient permissions
@HttpCode(404) // Not Found - resource doesn't exist
@HttpCode(500) // Internal Server Error - server errors
Response Headers
Use the @Header() decorator to set custom response headers.
@Header Decorator
The @Header() decorator is a method decorator that sets HTTP response headers.
Purpose: Add custom headers to responses (e.g., caching, CORS, custom metadata).
How it works:
- Sets a header in the response
- Applied at the method level
- Can be used multiple times for different headers
import { Controller, Get, Header } from '@hazeljs/core';
@Controller('users')
export class UsersController {
@Get()
@Header('Cache-Control', 'no-cache, no-store')
// Sets Cache-Control header to prevent caching
findAll() {
return 'This action returns all users';
}
@Get('cached')
@Header('Cache-Control', 'public, max-age=3600')
// Cache for 1 hour
findAllCached() {
return 'This action returns cached users';
}
@Get('custom')
@Header('X-Custom-Header', 'custom-value')
// Custom header
findAllCustom() {
return 'This action returns all users';
}
}
Multiple Headers:
@Get()
@Header('Cache-Control', 'no-cache')
@Header('X-API-Version', 'v1')
@Header('X-Request-ID', '12345')
findAll() {
// Sets multiple headers
return 'This action returns all users';
}
Common Use Cases:
// CORS headers
@Header('Access-Control-Allow-Origin', '*')
// Content type
@Header('Content-Type', 'application/json')
// Rate limiting
@Header('X-RateLimit-Limit', '100')
@Header('X-RateLimit-Remaining', '99')
// Custom metadata
@Header('X-Request-ID', requestId)
Redirection
Use the @Redirect() decorator to redirect responses to a different URL.
@Redirect Decorator
The @Redirect() decorator is a method decorator that redirects the client to a different URL.
Purpose: Redirect requests to new URLs (e.g., URL changes, temporary redirects).
How it works:
- Sends a redirect response with status code
- Client automatically follows the redirect
- Can be absolute or relative URL
import { Controller, Get, Redirect } from '@hazeljs/core';
@Controller('users')
export class UsersController {
@Get('old-route')
@Redirect('/users/new-route', 301)
// 301 = Permanent Redirect (Moved Permanently)
// Client will cache the redirect
oldRoute() {
// This will redirect to /users/new-route
// Method body is not executed
}
@Get('temporary')
@Redirect('https://example.com', 302)
// 302 = Temporary Redirect (Found)
// Client will not cache the redirect
temporaryRedirect() {
// Redirects to external URL
}
}
Status Codes:
- 301 Moved Permanently: Permanent redirect (cached by browsers)
- 302 Found: Temporary redirect (not cached)
- 307 Temporary Redirect: Temporary redirect (preserves method)
- 308 Permanent Redirect: Permanent redirect (preserves method)
Async / Await
HazelJS fully supports async/await. Every handler method can be async and return a Promise.
How it works:
- HazelJS automatically awaits async methods
- Return values are serialized to JSON
- Errors are caught and handled by exception filters
@Get()
// Method can be async
async findAll(): Promise<User[]> {
// Return a Promise - HazelJS will await it
return await this.usersService.findAll();
}
@Get(':id')
async findOne(@Param('id') id: string): Promise<User> {
// Async operations are fully supported
const user = await this.usersService.findOne(parseInt(id));
if (!user) {
throw new NotFoundException(`User #${id} not found`);
}
return user;
}
Error Handling:
@Get()
async findAll() {
try {
return await this.usersService.findAll();
} catch (error) {
// Errors can be caught and handled
// Or let exception filters handle them
throw new InternalServerErrorException('Failed to fetch users');
}
}
Complete Example
Here's a complete controller demonstrating all features:
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
Query,
HttpCode,
Header,
NotFoundException,
} from '@hazeljs/core';
interface User {
id: number;
name: string;
email: string;
}
interface CreateUserDto {
name: string;
email: string;
}
interface UpdateUserDto {
name?: string;
email?: string;
}
@Controller('users')
export class UsersController {
private users: User[] = [
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' },
];
// GET /users?search=john
@Get()
findAll(@Query('search') search?: string) {
if (search) {
return this.users.filter(
(user) =>
user.name.toLowerCase().includes(search.toLowerCase()) ||
user.email.toLowerCase().includes(search.toLowerCase()),
);
}
return this.users;
}
// GET /users/:id
@Get(':id')
findOne(@Param('id') id: string) {
const user = this.users.find((u) => u.id === parseInt(id));
if (!user) {
throw new NotFoundException(`User #${id} not found`);
}
return user;
}
// POST /users
@Post()
@HttpCode(201)
create(@Body() createUserDto: CreateUserDto) {
const newUser: User = {
id: this.users.length + 1,
...createUserDto,
};
this.users.push(newUser);
return newUser;
}
// PUT /users/:id
@Put(':id')
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
const user = this.users.find((u) => u.id === parseInt(id));
if (!user) {
throw new NotFoundException(`User #${id} not found`);
}
Object.assign(user, updateUserDto);
return user;
}
// DELETE /users/:id
@Delete(':id')
@HttpCode(204)
remove(@Param('id') id: string) {
const index = this.users.findIndex((u) => u.id === parseInt(id));
if (index === -1) {
throw new NotFoundException(`User #${id} not found`);
}
this.users.splice(index, 1);
}
}
Try It Yourself
Create a new file app.controller.ts:
import { Controller, Get } from '@hazeljs/core';
@Controller()
export class AppController {
@Get()
getHello() {
return { message: 'Hello from HazelJS!' };
}
}
Register it in your module:
import { HazelModule } from '@hazeljs/core';
import { AppController } from './app.controller';
@HazelModule({
controllers: [AppController],
})
export class AppModule {}
Start your application and visit http://localhost:3000 - you should see the JSON response!
Best Practices
-
Keep Controllers Thin: Controllers should only handle HTTP concerns. Move business logic to services.
-
Use DTOs: Define Data Transfer Objects for request/response validation.
-
Handle Errors: Use exception filters for consistent error handling.
-
Type Safety: Use TypeScript interfaces and classes for type safety.
-
Async Operations: Use async/await for all asynchronous operations.
-
Status Codes: Use appropriate HTTP status codes for different scenarios.
-
Documentation: Document your endpoints using Swagger decorators.
What's Next?
- Learn about Providers to add business logic
- Understand Modules to organize your application
- Add Validation to validate request data
- Implement Exception Filters for error handling