NZOTH: Nest + Zod + OpenAPI + Typed + Helpers (pronounced "n'zoth")
A collection of NestJS utilities for building RESTful APIs with support for filtering, pagination, sorting, type-safe request/response validation, and OpenAPI schema registration using Zod. Allows registering Zod schemas with OpenAPI metadata for automatic documentation generation.
NZOTH combines the best of all worlds:
- NestJS: The progressive Node.js framework for building efficient, reliable, and scalable server-side applications
- Zod: TypeScript-first schema validation with static type inference
- OpenAPI: Automated API documentation that stays in sync with your code
- Typed: End-to-end type safety from your routes to your controllers to your clients
- Helpers: A comprehensive collection of utilities for filtering, pagination, sorting, and more
Our goal is to provide a type-safe API solution that's easy to use while still being compatible with various languages and tools. NZOTH offers a simple yet powerful alternative to solutions like tRPC and GraphQL for projects that need REST APIs with strong typing.
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.
- ✅ 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
npm install @lonestone/nzoth
# or
yarn add @lonestone/nzoth
# or
pnpm add @lonestone/nzoth
.openapi()
method to all Zod types. This is a global modification that affects all Zod instances in your application. This is done to provide a seamless integration with OpenAPI documentation generation see here.
First, define your schemas using z
(which adds OpenAPI metadata support to Zod):
// Define an enum with OpenAPI metadata
export const UserRole = z.enum(['admin', 'user']).openapi({
title: 'UserRole',
description: 'User role',
example: 'admin',
});
// Define the main schema with OpenAPI metadata
export const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(2),
email: z.string().email(),
age: z.number().positive(),
role: UserRole,
tags: z.array(z.string())
}).openapi({
title: 'User',
description: 'User schema',
});
// Create derived schemas
export const UserCreateSchema = z.openapi(UserSchema.omit({ id: true }), {
title: 'UserCreate',
description: 'User create schema',
});
export const UserUpdateSchema = z.openapi(UserSchema.omit({ id: true }), {
title: 'UserUpdate',
description: 'User update schema',
});
// Export types
export type User = z.infer<typeof UserSchema>;
export type UserCreate = z.infer<typeof UserCreateSchema>;
export type UserUpdate = z.infer<typeof UserUpdateSchema>;
Use TypedRoute
decorators to validate response data:
@Controller('users')
class UserController {
@TypedRoute.Get(undefined, UsersSchema)
findAll(
@FilteringParams(userFilteringSchema) filters?: any[],
@PaginationParams(userPaginationSchema) pagination?: any,
@SortingParams(userSortingSchema) sort?: any[],
): User[] {
// Response will be validated against UsersSchema
return this.userService.findAll(filters, pagination, sort);
}
@TypedRoute.Get(':id', UserSchema)
findOne(@TypedParam('id', 'uuid') id: string): User {
// Response will be validated against UserSchema
return this.userService.findOne(id);
}
@TypedRoute.Post('', UserSchema)
create(@TypedBody(UserCreateSchema) userData: UserCreate): User {
// Request body validated against UserCreateSchema
// Response validated against UserSchema
return this.userService.create(userData);
}
@TypedRoute.Put(':id', UserUpdateSchema)
update(
@TypedParam('id', 'uuid') id: string,
@TypedBody(UserUpdateSchema) userData: UserUpdate
): User {
// Both request and response are validated
return this.userService.update(id, userData);
}
@TypedRoute.Delete(':id')
remove(@TypedParam('id', 'uuid') id: string) {
return this.userService.remove(id);
}
}
Define your filtering, pagination, and sorting schemas:
// Filtering
export const enabledUserFilteringKeys = [
"name",
"email",
"role",
"tags",
] as const;
export const userFilteringSchema = createFilterQueryStringSchema(
enabledUserFilteringKeys
);
// Sorting
export const enabledUserSortingKeys = [
"name",
"email",
"role",
"tags",
];
export const userSortingSchema = createSortingQueryStringSchema(
enabledUserSortingKeys
);
// Pagination
export const userPaginationSchema = createPaginationQuerySchema({
defaultPageSize: 10,
maxPageSize: 100,
minPageSize: 1,
});
export type UserPagination = z.infer<typeof userPaginationSchema>;
Supported filter rules:
eq
- Equalsneq
- Not equalsgt
- Greater thangte
- Greater than or equalslt
- Less thanlte
- Less than or equalslike
- Like (string pattern matching)nlike