Overview
Controllers in Postiz handle HTTP requests and responses. They are the entry point for all API endpoints and follow strict patterns for consistency and maintainability.
Controller Structure
Controllers are located in apps/backend/src/api/routes/:
apps/backend/src/api/routes/
├── auth.controller.ts
├── posts.controller.ts
├── analytics.controller.ts
├── integrations.controller.ts
└── ... (other controllers)
Basic Controller Pattern
Here’s the standard pattern for creating a controller:
import {
Body ,
Controller ,
Get ,
Post ,
Param ,
Query ,
Req ,
Res ,
} from '@nestjs/common' ;
import { Response , Request } from 'express' ;
import { ApiTags } from '@nestjs/swagger' ;
import { AuthService } from '@gitroom/backend/services/auth/auth.service' ;
import { CreateOrgUserDto } from '@gitroom/nestjs-libraries/dtos/auth/create.org.user.dto' ;
import { RealIP } from 'nestjs-real-ip' ;
import { UserAgent } from '@gitroom/nestjs-libraries/user/user.agent' ;
@ ApiTags ( 'Auth' )
@ Controller ( '/auth' )
export class AuthController {
constructor (
private _authService : AuthService ,
private _emailService : EmailService
) {}
@ Get ( '/can-register' )
async canRegister () {
return {
register: await this . _authService . canRegister ( 'LOCAL' ),
};
}
@ Post ( '/register' )
async register (
@ Req () req : Request ,
@ Body () body : CreateOrgUserDto ,
@ Res ({ passthrough: false }) response : Response ,
@ RealIP () ip : string ,
@ UserAgent () userAgent : string
) {
try {
const { jwt , addedOrg } = await this . _authService . routeAuth (
body . provider ,
body ,
ip ,
userAgent
);
response . cookie ( 'auth' , jwt , {
domain: getCookieUrlFromDomain ( process . env . FRONTEND_URL ! ),
secure: true ,
httpOnly: true ,
sameSite: 'none' ,
expires: new Date ( Date . now () + 1000 * 60 * 60 * 24 * 365 ),
});
response . status ( 200 ). json ({ register: true });
} catch ( e : any ) {
response . status ( 400 ). send ( e . message );
}
}
}
Key Decorators
Class-level Decorators
@ ApiTags ( 'Auth' )
@ Controller ( '/auth' )
export class AuthController {}
Method-level Decorators
@ Get ( '/list' ) // GET /posts/list
@ Post ( '/create' ) // POST /posts/create
@ Put ( '/update/:id' ) // PUT /posts/update/:id
@ Delete ( '/delete/:id' ) // DELETE /posts/delete/:id
@ Patch ( '/patch/:id' ) // PATCH /posts/patch/:id
Path Parameters
@ Get ( '/post/:id' )
async getPost (@ Param ( 'id' ) id : string ) {
return this . _postsService . getById ( id );
}
Query Parameters
@ Get ( '/posts' )
async getPosts (
@ Query ( 'page' ) page : number = 1 ,
@ Query ( 'limit' ) limit : number = 10 ,
@ Query ( 'search' ) search ?: string ,
) {
return this . _postsService . list ({ page , limit , search });
}
Request Body
@ Post ( '/create' )
async createPost (@ Body () body : CreatePostDto ) {
return this . _postsService . create ( body );
}
Always use DTOs (Data Transfer Objects) for request body validation. DTOs are located in libraries/nestjs-libraries/src/dtos/.
@ Post ( '/login' )
async login (
@ RealIP () ip : string , // Real IP address
@ UserAgent () userAgent : string , // User agent string
@ Body () body : LoginUserDto ,
) {
return this . _authService . login ( body , ip , userAgent );
}
Response Handling
Simple JSON Response
Manual Response Control
@ Post ( '/register' )
async register (
@ Body () body : CreateOrgUserDto ,
@ Res ({ passthrough: false }) response : Response ,
) {
try {
const result = await this . _authService . register ( body );
// Set custom headers
response . header ( 'onboarding' , 'true' );
// Set cookies
response . cookie ( 'auth' , result . jwt , {
httpOnly: true ,
secure: true ,
});
// Send response
response . status ( 200 ). json ({ success: true });
} catch ( e : any ) {
response . status ( 400 ). send ( e . message );
}
}
When using @Res({ passthrough: false }), you must manually call response.json() or response.send(). NestJS will not automatically serialize the return value.
Setting Cookies
response . cookie ( 'auth' , jwt , {
domain: getCookieUrlFromDomain ( process . env . FRONTEND_URL ! ),
... ( ! process . env . NOT_SECURED
? {
secure: true ,
httpOnly: true ,
sameSite: 'none' ,
}
: {}),
expires: new Date ( Date . now () + 1000 * 60 * 60 * 24 * 365 ), // 1 year
});
Error Handling
Try-Catch Pattern
@ Post ( '/create' )
async createPost (@ Body () body : CreatePostDto ) {
try {
return await this . _postsService . create ( body );
} catch ( e : any ) {
throw new Error ( e . message );
}
}
HTTP Exceptions
import {
BadRequestException ,
NotFoundException ,
UnauthorizedException ,
ForbiddenException ,
} from '@nestjs/common' ;
@ Get ( '/post/:id' )
async getPost (@ Param ( 'id' ) id : string ) {
const post = await this . _postsService . findById ( id );
if ( ! post ) {
throw new NotFoundException ( 'Post not found' );
}
return post ;
}
Dependency Injection
Constructor Injection
export class PostsController {
constructor (
private _postsService : PostsService ,
private _analyticsService : AnalyticsService ,
private _mediaService : MediaService ,
) {}
}
Services are injected via the constructor. Use the private keyword to automatically create and assign class properties.
Validation with DTOs
Creating a DTO
DTOs are located in libraries/nestjs-libraries/src/dtos/:
import { IsString , IsOptional , IsArray , IsDateString } from 'class-validator' ;
import { ApiProperty } from '@nestjs/swagger' ;
export class CreatePostDto {
@ ApiProperty ()
@ IsString ()
content : string ;
@ ApiProperty ({ required: false })
@ IsOptional ()
@ IsString ()
image ?: string ;
@ ApiProperty ()
@ IsArray ()
integrations : string [];
@ ApiProperty ()
@ IsDateString ()
scheduledAt : string ;
}
Using DTOs in Controllers
@ Post ( '/create' )
async createPost (@ Body () body : CreatePostDto ) {
// body is automatically validated
return this . _postsService . create ( body );
}
OAuth Flow Example
@ Get ( '/oauth/:provider' )
async oauthLink (
@ Param ( 'provider' ) provider : string ,
@ Query () query : any ,
) {
return this . _authService . oauthLink ( provider , query );
}
@ Post ( '/oauth/:provider/exists' )
async oauthExists (
@ Body ( 'code' ) code : string ,
@ Param ( 'provider' ) provider : string ,
@ Res ({ passthrough: false }) response : Response ,
) {
const { jwt , token } = await this . _authService . checkExists ( provider , code );
if ( token ) {
return response . json ({ token });
}
response . cookie ( 'auth' , jwt , {
domain: getCookieUrlFromDomain ( process . env . FRONTEND_URL ! ),
secure: true ,
httpOnly: true ,
sameSite: 'none' ,
expires: new Date ( Date . now () + 1000 * 60 * 60 * 24 * 365 ),
});
response . header ( 'reload' , 'true' );
response . status ( 200 ). json ({ login: true });
}
Swagger Documentation
Basic Documentation
import { ApiTags , ApiOperation , ApiResponse } from '@nestjs/swagger' ;
@ ApiTags ( 'Posts' )
@ Controller ( '/posts' )
export class PostsController {
@ Get ( '/list' )
@ ApiOperation ({ summary: 'Get all posts' })
@ ApiResponse ({ status: 200 , description: 'List of posts' })
@ ApiResponse ({ status: 401 , description: 'Unauthorized' })
async getPosts () {
return this . _postsService . list ();
}
}
Real-World Example
Here’s a complete controller from the codebase:
import {
Body ,
Controller ,
Get ,
Param ,
Post ,
Res ,
} from '@nestjs/common' ;
import { Response } from 'express' ;
import { ApiTags } from '@nestjs/swagger' ;
import { AuthService } from '@gitroom/backend/services/auth/auth.service' ;
import { ForgotPasswordDto } from '@gitroom/nestjs-libraries/dtos/auth/forgot.password.dto' ;
import { ForgotReturnPasswordDto } from '@gitroom/nestjs-libraries/dtos/auth/forgot-return.password.dto' ;
@ ApiTags ( 'Auth' )
@ Controller ( '/auth' )
export class AuthController {
constructor ( private _authService : AuthService ) {}
@ Post ( '/forgot' )
async forgot (@ Body () body : ForgotPasswordDto ) {
try {
await this . _authService . forgot ( body . email );
return { forgot: true };
} catch ( e ) {
return { forgot: false };
}
}
@ Post ( '/forgot-return' )
async forgotReturn (@ Body () body : ForgotReturnPasswordDto ) {
const reset = await this . _authService . forgotReturn ( body );
return { reset: !! reset };
}
@ Post ( '/activate' )
async activate (
@ Body ( 'code' ) code : string ,
@ Res ({ passthrough: false }) response : Response ,
) {
const activate = await this . _authService . activate ( code );
if ( ! activate ) {
return response . status ( 200 ). json ({ can: false });
}
response . cookie ( 'auth' , activate , {
domain: getCookieUrlFromDomain ( process . env . FRONTEND_URL ! ),
secure: true ,
httpOnly: true ,
sameSite: 'none' ,
expires: new Date ( Date . now () + 1000 * 60 * 60 * 24 * 365 ),
});
response . header ( 'onboarding' , 'true' );
return response . status ( 200 ). json ({ can: true });
}
}
Best Practices
Keep controllers thin
Controllers should only handle HTTP concerns. Business logic belongs in services.
Use DTOs for validation
Always validate input using class-validator DTOs.
Follow the layer pattern
Controller → Service → Repository. Never skip layers.
Handle errors properly
Use try-catch blocks and appropriate HTTP exceptions.
Document with Swagger
Use @ApiTags, @ApiOperation, and @ApiResponse decorators.
Use dependency injection
Inject services via constructor, never instantiate manually.
Next Steps
Service Patterns Learn how to implement service layer logic
Database Schema Understand the database structure