-
Notifications
You must be signed in to change notification settings - Fork 0
Description
I feel like the repo is missing a centralized documentation.
Here is what I had in my previous codebase (things should be easy to adapt for nzoth):
# @sunagenda/nest-rest-helpers
A collection of NestJS utilities for building RESTful APIs with support for filtering, pagination, sorting, and type-safe request/response validation.
# Why this package ?
Our goal is to have a type-safe API which is easy to use while still being usable by other languages. tRPC is a great tool, but using it with other tools (low-code, AI) or other languages is not straightforward.
GraphQL is also awesome, but for most project it's overkill.
We aimed to keep things simple, with the following goals:
- Things should be as type-safe as possible;
- It should allow developers to follow the code as much as possible (ctrl+click on a DTO should bring them to the implementation);
- It should be easy to use in a monorepo environment;
- It should provide a standard way to paginate, filter and sort data;
- It should allow for data validation, input and output;
- It should help to get a CRUD API up and running quickly;
- It should be able to generate OpenAPI documentation from your Zod schemas, without modifying NestJS Swagger behavior;
- In general, it should be as unobtrusive as possible.
We use a lot of NestJS, and we copy/pasted a similar structure from project to project, but we never took the time to really make it a package.
# Features
- ✅ **Validation**: Comprehensive Zod-based validation for requests and responses
- 🎯 **Decorators**: Decorators to validate route response, parameters, query params, and body
- 🔍 **Filtering**: Flexible query parameter-based filtering with multiple operators
- 📄 **Pagination**: Easy-to-use pagination with offset and page size
- 🔃 **Sorting**: Multi-field sorting with ascending/descending support
- 📄 **OpenAPI**: Automatically generate OpenAPI documentation from your Zod schemas, without modifying NestJS Swagger behavior
# Installation
```bash
pnpm add @sunagenda/nest-rest-helpersType safety and validation
Validate response data
Use TypedRoute decorators to validate response data:
@Controller('users')
class UserController {
@TypedRoute.Get(undefined, UserSchema)
findAll(): User[] {
// Response will be validated against UserSchema
return this.userService.findAll()
}
@TypedRoute.Post('/', UserSchema)
create(@TypedBody(CreateUserSchema) dto: CreateUserDto): User {
// Both request and response are validated
return this.userService.create(dto)
}
}Request Validation
Body Validation
Two decorators are available for body validation:
JSON Body (@TypedBody)
const CreateUserSchema = z.object({
name: z.string().min(3),
email: z.string().email(),
role: z.enum(['admin', 'user']),
})
@Controller('users')
class UserController {
@Post()
create(@TypedBody(CreateUserSchema) user: z.infer<typeof CreateUserSchema>) {
// user is validated against CreateUserSchema
// Content-Type must be application/json
return this.userService.create(user)
}
}Form URL-Encoded Body (@TypedFormBody)
const CreateArticleSchema = z.object({
title: z.string().min(3),
content: z.string(),
tags: z.array(z.string()),
})
@Controller('articles')
class ArticleController {
@Post()
create(@TypedFormBody(CreateArticleSchema) data: z.infer<typeof CreateArticleSchema>) {
// data is validated against CreateArticleSchema
// Content-Type must be application/x-www-form-urlencoded
return this.articleService.create(data)
}
}Query Parameters
// Example query url: /users?q=test&tags=tag1&tags=tag2&page=1
@Controller('users')
class UserController {
@Get()
search(
@TypedQuery('q', z.string().min(2)) query: string,
@TypedQuery('tags', z.string(), { array: true }) tags: string[],
@TypedQuery('page', z.coerce.number().int().positive(), { optional: true }) page?: number,
) {
return this.userService.search(query, tags, page)
}
}For typed query objects, you can use the TypedQueryObject decorator:
// Example query url: /users?q=test&tags=tag1&tags=tag2&page=1
@Controller('users')
class UserController {
@Get()
search(@TypedQueryObject(SearchQuery) query: z.infer<typeof SearchQuery>) {
// Query will be an object with the following shape: { q: string, tags: string[], page: number }
return this.userService.search(query)
}
}Route Parameters
// Example route url: /users/123
@Controller('users')
class UserController {
@Get(':id')
findOne(@TypedParam('id', 'uuid') id: string) {
// id is validated as UUID
return this.userService.findOne(id)
}
}CRUD helpers
Filtering
Use the @FilteringParams decorator for query-based filtering:
// Example query url: /users?filter=name:eq:john;age:gt:18
@Controller('users')
class UserController {
@Get()
findAll(@FilteringParams(['name', 'age']) filters?: Filtering[]) {
// filters will be an array of { property, rule, value }
return this.userService.findAll(filters)
}
}Supported filter rules:
eq- Equalsneq- Not equalsgt- Greater thangte- Greater than or equalslt- Less thanlte- Less than or equalslike- Like (string pattern matching)nlike- Not likein- In arraynin- Not in arrayisnull- Is nullisnotnull- Is not null
Query parameter format: ?filter=property:rule:value
See the Filtering file for more details.
Pagination
Use the @PaginationParams decorator to handle pagination:
// Example query url: /users?offset=0&pageSize=10
@Controller('users')
class UserController {
@Get()
findAll(@PaginationParams() pagination: Pagination) {
// pagination will contain { offset, pageSize }
return this.userService.findAll(pagination)
}
}Query parameter format: ?offset=0&pageSize=10
The response will be wrapped in a paginated format:
interface PaginatedResponse<T> {
data: T[]
meta: {
offset: number
pageSize: number
itemCount: number
hasMore: boolean
}
}See the Pagination file for more details.
Sorting
Use the @SortingParams decorator for query-based sorting:
// Example query url: /users?sort=name:asc,age:desc
@Controller('users')
class UserController {
@Get()
findAll(@SortingParams(['name', 'age']) sort?: Sort[]) {
// sort will be an array of { property, direction }
return this.userService.findAll(sort)
}
}Query parameter format: ?sort=property:direction
See the Sorting file for more details.
Error Handling
The package includes built-in exception filters for validation errors:
// In your app.module.ts or main.ts
app.useGlobalFilters(
new ZodValidationExceptionFilter(),
new ZodSerializationExceptionFilter()
)Error responses will include detailed validation errors:
{
"statusCode": 400,
"timestamp": "2024-01-01T00:00:00.000Z",
"path": "/api/users",
"errors": [
{
"code": "invalid_type",
"message": "Expected string, received number",
"path": ["name"]
}
]
}OpenAPI Documentation
All decorators automatically generate OpenAPI documentation from your Zod schemas. Under the hood, they use the @ApiResponse, @ApiBody, @ApiQuery, @ApiParam decorators from NestJS Swagger and an OpenAPI schema generated by zod-openapi.
Important notes
This package requires OpenAPI 3.1.0, because zod-openapi is not compatible with OpenAPI 3.0.0 (it will show "Unknown type: object" errors).
So set your swagger version to 3.1.0 while booting your NestJS app:
const config = new DocumentBuilder()
.setOpenAPIVersion('3.1.0')
.setTitle('Docs Swagger')
.setVersion('1.0.0')
.addBearerAuth()
.build()Override Response Documentation
You can override the generated documentation using NestJS Swagger decorators:
@Controller('users')
class UserController {
@ApiResponse({
status: 200,
description: 'Custom description',
content: {
'application/json': {
examples: {
user: { value: { id: '123', name: 'John' } }
}
}
}
})
@TypedRoute.Get(':id', UserSchema)
findOne(@TypedParam('id', 'uuid') id: string) {
return this.userService.findOne(id)
}
}Override Body Documentation
@Controller('users')
class UserController {
@ApiBody({
description: 'Custom description',
examples: {
user: {
value: { name: 'John', email: 'john@example.com' }
}
}
})
@Post()
create(@TypedBody(CreateUserSchema) user: z.infer<typeof CreateUserSchema>) {
return this.userService.create(user)
}
}Override Query Documentation
@Controller('users')
class UserController {
@ApiQuery({
name: 'search',
description: 'Custom description',
example: 'john'
})
@Get()
search(@TypedQuery('search', z.string().min(2)) query: string) {
return this.userService.search(query)
}
}Override Parameter Documentation
@Controller('users')
class UserController {
@ApiParam({
name: 'id',
description: 'Custom description',
example: '123e4567-e89b-12d3-a456-426614174000'
})
@Get(':id')
findOne(@TypedParam('id', 'uuid') id: string) {
return this.userService.findOne(id)
}
}License
UNLICENSED - ©Lonestone - Pierrick Bignet