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 significations:
- 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.meta() 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 metadataexportconstUserRole=z.enum(['admin','user']).meta({title: 'UserRole',description: 'User role',example: 'admin',});// Define the main schema with OpenAPI metadataexportconstUserSchema=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()),}).meta({title: 'User',description: 'User schema',});// Create derived schemasexportconstUserCreateSchema=z.meta(UserSchema.omit({id: true}),{title: 'UserCreate',description: 'User create schema',});exportconstUserUpdateSchema=z.meta(UserSchema.omit({id: true}),{title: 'UserUpdate',description: 'User update schema',});// Export typesexporttypeUser=z.infer<typeofUserSchema>;exporttypeUserCreate=z.infer<typeofUserCreateSchema>;exporttypeUserUpdate=z.infer<typeofUserUpdateSchema>;Use TypedRoute decorators to validate response data:
// All routes in this controller will require the clientId parameter// But we do not need to define it in the route path if not needed for the route logic (See put)// Ownership should be checked in a Guard, not inside the controller
@TypedController('/clients/:clientId/users',z.object({clientId: z.string().uuid(),}),)classUserController{
@TypedRoute.Get(undefined,UsersSchema)findAll(
@TypedParam('clientId')clientId: string,
@FilteringParams(userFilteringSchema)filters?: any[],
@PaginationParams(userPaginationSchema)pagination?: any,
@SortingParams(userSortingSchema)sort?: any[],): User[]{// Response will be validated against UsersSchemareturnthis.userService.findAll(clientId,filters,pagination,sort);}
@TypedRoute.Get(':id',UserSchema)findOne(@TypedParam('id','uuid')id: string): User{// Response will be validated against UserSchemareturnthis.userService.findOne(id);}
@TypedRoute.Post('',UserSchema)create(
@TypedParam('clientId')clientId: string,
@TypedBody(UserCreateSchema)userData: UserCreate,): User{// Request body validated against UserCreateSchema// Response validated against UserSchemareturnthis.userService.create(clientId,userData);}
@TypedRoute.Put(':id',UserUpdateSchema)update(
@TypedParam('id','uuid')id: string,
@TypedBody(UserUpdateSchema)userData: UserUpdate,): User{// Both request and response are validatedreturnthis.userService.update(id,userData);}
@TypedRoute.Delete(':id')remove(@TypedParam('id','uuid')id: string){returnthis.userService.remove(id);}}Define your filtering, pagination, and sorting schemas:
// FilteringexportconstenabledUserFilteringKeys=['name','email','role','tags']asconst;exportconstuserFilteringSchema=createFilterQueryStringSchema(enabledUserFilteringKeys);// SortingexportconstenabledUserSortingKeys=['name','email','role','tags'];exportconstuserSortingSchema=createSortingQueryStringSchema(enabledUserSortingKeys);// PaginationexportconstuserPaginationSchema=createPaginationQuerySchema({defaultPageSize: 10,maxPageSize: 100,minPageSize: 1,});exporttypeUserPagination=z.infer<typeofuserPaginationSchema>;Supported filter rules:
eq- Equalsneq- Not equalsgt- Greater thangte- Greater than or equalslt- Less thanlte- Less than or equalslike- Like (string pattern matching)nlike
Built on top of:
Logo by Alexandra Chaillou