@nestify-js/shared
v0.3.8
Published
Shared utilities and types for nestify-js packages
Downloads
419
Maintainers
Readme
Nestify
⚠️ Warning: This is not an official release version. APIs may change in the future.
Nestify is a NestJS-like dependency injection framework for Fastify that uses modern Stage 3 decorators instead of the legacy decorators used by NestJS.
This project was created because NestJS uses the old decorator syntax, but we wanted to leverage the new Stage 3 decorator specification for better type safety and modern JavaScript features.
Installation
pnpm add nestify-jsAPI Documentation
Using of decorators looks basically like they are in NestJS, but with modern Stage 3 syntax.
Note: It is recommended to set "strictPropertyInitialization": false in your tsconfig.json to avoid linting issues when using property injection.
HTTP Method Decorators
These decorators are used to define HTTP routes on controller methods:
import { Get, Post, Put, Patch, Delete, HttpMethod } from 'nestify-js';
@Controller('/api')
class UserController {
@Get('/users')
getUsers() {
return { users: [] };
}
@Post('/users')
createUser() {
return { message: 'User created' };
}
@Put('/users/:id')
updateUser() {
return { message: 'User updated' };
}
@Patch('/users/:id')
patchUser() {
return { message: 'User patched' };
}
@Delete('/users/:id')
deleteUser() {
return { message: 'User deleted' };
}
@(HttpMethod('OPTIONS')('/users'))
optionsUsers() {
return { methods: ['GET', 'POST'] };
}
}Route Configuration
@Controller(prefix?: string)
Marks a class as a controller and optionally sets a route prefix:
@Controller('/api/v1')
class ApiController {
@Get('/health')
health() {
return { status: 'ok' };
}
}
// This creates route: GET /api/v1/health@ApiSchema(schema)
Sets OpenAPI/Swagger schema information for routes:
@Controller('/users')
class UserController {
@Get('/:id')
@ApiSchema({
summary: 'Get user by ID',
description: 'Retrieves a user by their unique identifier',
tags: ['users'],
})
getUser() {
return { user: {} };
}
}@Opt(options)
Sets additional Fastify route options:
@Controller('/files')
class FileController {
@Post('/upload')
@Opt({
bodyLimit: 1048576, // 1MB
attachValidation: true,
})
uploadFile() {
return { uploaded: true };
}
}Dependency Injection
@Injectable()
Marks a class as a service that can be injected:
@Injectable()
class UserService {
getUsers() {
return [{ id: 1, name: 'John' }];
}
}@Inject(token)
Injects dependencies into class properties:
@Injectable()
class UserController {
@Inject(UserService)
userService: UserService; // here might be linted by typescript, you can set "strictPropertyInitialization": false in tsconfig.json
@Inject('DATABASE_URL')
databaseUrl: string;
getUsers() {
return this.userService.getUsers();
}
}@Module(options)
Defines a module with providers, controllers, imports, and exports:
@Module({
imports: [DatabaseModule],
providers: [UserService],
controllers: [UserController],
exports: [UserService],
})
class UserModule {}Middleware System
There are four kinds of middleware: Guards, Interceptors, Pipes and Filters.
Execution order of a single request:
Request → Interceptor(enter) → Guard → Pipe → Controller method → Interceptor(leave)
└────────── Exception → Filter ──────────┘Registration Rules (Important)
- Built-in middlewares are auto-registered: the framework's preset pipes (
PipeBody/PipeQuery/PipeParams/PipeIp/PipeRaw/PipeFile) andJwtGuardare automatically instantiated duringapply(). They work out of the box, no configuration needed. - Custom middlewares must be registered: like NestJS, classes decorated by
@Guard()/@Interceptor()/@Pipe()/@Filter()must appear in some module'sproviders, otherwise route registration fails withCannot find class for token. - Where to apply:
@UseGuards/@UseInterceptors/@UsePipes/@UseFilterscan be applied on a controller class (affects all its routes) or on a method (affects only that route). Middlewares of the same kind run in order: global → controller → method. - Middleware classes are
Injectabletoo, so@Injectproperty injection works inside them.
Guards
Guards control access to routes. Returning false or throwing from canActivate aborts the request:
@Guard()
class AuthGuard implements NestifyGuard {
// Dependency injection works
@Inject(AuthService)
authService: AuthService;
canActivate(context: ExecutionContext): boolean | Promise<boolean> {
const request = context.switchToHttp().getRequest();
return request.headers.authorization === 'Bearer valid-token';
// Or throw new UnauthorizedException() for a specific error
}
}
// Must be registered in providers before use
@Module({ controllers: [AdminController], providers: [AuthGuard] })
class AdminModule {}
@Controller('/admin')
@UseGuards(AuthGuard) // Controller level: applies to all routes
class AdminController {
@Get('/dashboard')
getDashboard() {
return { data: 'sensitive' };
}
@Get('/stats')
@UseGuards(AnotherGuard) // Method level: appended after controller-level guards
getStats() {
return { data: 'stats' };
}
}Register a global guard with the APP_GUARD token to guard every route (each kind of global middleware can only be registered once):
@Module({
controllers: [AppController],
providers: [{ provide: APP_GUARD, useClass: AuthGuard }],
})
class AppModule {}Interceptors
Interceptors run before the controller method; the returned function is called after the method finishes (useful for logging, timing, response wrapping). The returned function receives the handler's return value (or the caught error):
@Interceptor()
class LoggingInterceptor implements NestifyInterceptor {
intercept(context: ExecutionContext) {
const start = Date.now();
console.log('Request started');
return (result: any) => {
console.log(`Request completed in ${Date.now() - start}ms`);
return result; // Can be modified; the return value becomes the final response
};
}
}
@Controller('/api')
@UseInterceptors(LoggingInterceptor)
class ApiController {
@Get('/data')
getData() {
return { data: 'example' };
}
}Global interceptor: { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor }.
Pipes
Pipes validate and transform input data. Each pipe's return value becomes the next pipe's input.
Custom pipes are applied via @UsePipes, optionally with a validation schema (validation is based on fastify's validatorCompiler):
@Pipe()
class TrimPipe implements NestifyPipe {
async transform(context: ExecutionContext, input: any[], schema?: PipeFullSchema) {
// `input` comes from the previous step; the return value goes to the next pipe or the handler
return input.map((v) => (typeof v === 'string' ? v.trim() : v));
}
}
@Controller('/users')
@UsePipes(TrimPipe) // Also works without a schema (transformation only)
class UserController {
@Post('/')
@UsePipes({
pipe: TrimPipe,
schema: { body: { type: 'object', required: ['name'] } }, // PipeOptions: pipe + schema
})
createUser() {
// ...
}
}Built-in pipes (auto-registered, use them directly) extract data from the request object and pass it to the handler:
@Controller('/users')
class UserController {
// @Body(schema?, ok?, other?)
// - schema: JSON Schema to validate request.body
// - ok: generates the response.200 schema (for swagger)
// - other: remaining fastify route schema (e.g. headers, response)
@Post('/')
@Body({ type: 'object', required: ['name', 'email'] })
createUser(body: any) {
return { user: body }; // Handler receives request.body
}
@Get('/')
@Query({ type: 'object' })
getUsers(query: any) {
return { query }; // Handler receives request.query
}
@Get('/:id')
@Params({ type: 'object', required: ['id'] })
getUser(params: any) {
return { id: params.id }; // Handler receives request.params
}
@Get('/ip')
getUserIP(ip: string) {
return { ip }; // Handler receives request.ip
}
@Post('/raw')
handleRaw(raw: any) {
// @Raw(): handler receives request.raw (the raw Node request)
return { received: true };
}
}Note:
@Body/@Query/@Params/@Ip/@Rawignore the previous pipe's return value and always extract from therequestobject. When chaining pipes, put them last or handle the data yourself in a custom pipe.
Global pipe: { provide: APP_PIPE, useClass: MyPipe } (or { provide: APP_PIPE, useValue: { pipe: MyPipe, schema: {...} } }).
Filters
Filters handle exceptions thrown by routes. Specify the exception classes to catch in the decorator (omit to catch all):
@Filter(HttpException)
class HttpExceptionFilter implements NestifyFilter {
catch(context: ExecutionContext, exception: HttpException) {
const response = context.switchToHttp().getReply();
response.status(exception.status).send({
error: exception.message,
timestamp: new Date().toISOString(),
});
}
}
@Controller('/api')
@UseFilters(HttpExceptionFilter)
class ApiController {
@Get('/error')
throwError() {
throw new HttpException('Something went wrong', 400);
}
}Global filter: { provide: APP_FILTER, useClass: HttpExceptionFilter }.
Built-in JWT Guard
The framework ships with JwtGuard (auto-registered, no need to add it to providers). It extracts and verifies the token from Authorization: Bearer <token> and attaches the decoded payload to the request:
import { JwtGuard, JwtService, jwt } from 'nestify-js';
// `jwt` is the default JwtService instance; you can also pass your own: JwtGuard(myJwt)
@Controller('protected')
@UseGuards(JwtGuard())
class ProtectedController {
@Get('profile')
async getProfile(request: any) {
// The first handler argument is the pipe result; you can also read
// the request in guards/interceptors via context.switchToHttp().getRequest()
return request;
}
}ExecutionContext
All middlewares access request information through context: ExecutionContext:
const http = context.switchToHttp();
const request = http.getRequest<FastifyRequest>(); // fastify request object
const reply = http.getReply<FastifyReply>(); // fastify reply object
context.getClass(); // Current controller class
context.getHandler(); // Current handler methodApplication Bootstrap
nestify(rootModule, options?) (recommended)
Creates the fastify instance, registers fastify plugins, applies all modules and (optionally) starts listening — all in one call. Returns the underlying fastify instance.
import { nestify } from 'nestify-js';
const app = await nestify(AppModule, {
// shortcut for `fastify.logger`
logger: { level: 'info' },
// fastify plugins registered before modules are applied
// - tuple form: `[plugin]` or `[plugin, options]`
plugins: [
[multipart, { limits: { fileSize: 10 * 1024 * 1024 } }],
[staticFiles, { root: './public', prefix: '/' }],
],
// Setup callback to register auto-created instances (optional)
// - Built-in pipes and JwtGuard are auto-registered, usually not needed
// start listening after all modules are registered
// - `true` uses the `PORT` / `HOST` env vars (falling back to 3000 / 0.0.0.0)
listen: true,
// or override: listen: { port: 8080, host: 'localhost' }
});Available options:
| Option | Type | Description |
| ----------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
| logger | FastifyServerOptions['logger'] | Shortcut for fastify.logger |
| fastify | FastifyServerOptions | Options passed to the fastify factory (fastify(options)) |
| plugins | readonly [plugin, options?][] | Fastify plugins registered before modules are applied (callback-style and async-style are both accepted) |
| setup | (register: (cls: Constructor) => void) => void | Setup callback to register auto-created instances (optional; built-in pipes/JwtGuard are auto-registered, usually not needed) |
| listen | boolean \| Partial<FastifyListenOptions> | Start listening after all modules are registered |
| allowCrossModuleCircularReference | boolean | Must be true to allow cross-module circular dependencies (same-module circular references are always allowed). @default false |
apply(app, options)
If you need more control over the fastify instance (custom plugins, hooks, decorators...), create it yourself and use the lower-level apply() instead:
import fastify from 'fastify';
import { apply } from 'nestify-js';
const app = fastify({ logger: true });
await apply(app, { rootModule: AppModule });
await app.listen({ port: 3000 });Complete Usage Example
import {
Module,
Controller,
Injectable,
Inject,
Get,
Post,
Body,
Params,
UseGuards,
Guard,
nestify,
} from 'nestify-js';
// Service
@Injectable()
class UserService {
private users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
];
getUsers() {
return this.users;
}
getUserById(id: number) {
return this.users.find((user) => user.id === id);
}
createUser(userData: { name: string }) {
const user = { id: Date.now(), ...userData };
this.users.push(user);
return user;
}
}
// Guard
@Guard()
class AuthGuard {
canActivate(context) {
// Simple auth check
const request = context.switchToHttp().getRequest();
return request.headers.authorization === 'Bearer valid-token';
}
}
// Controller
@Controller('/api/users')
class UserController {
@Inject(UserService)
userService: UserService;
@Get('/')
getUsers() {
return this.userService.getUsers();
}
@Get('/:id')
@Params({
type: 'object',
properties: { id: { type: 'number' } },
required: ['id'],
})
getUser(@Params() params: { id: number }) {
return this.userService.getUserById(params.id);
}
@Post('/')
@UseGuards(AuthGuard)
@Body({
type: 'object',
properties: { name: { type: 'string' } },
required: ['name'],
})
createUser(@Body() body: { name: string }) {
return this.userService.createUser(body);
}
}
// Module
@Module({
providers: [UserService, AuthGuard],
controllers: [UserController],
})
class AppModule {}
// Application bootstrap
await nestify(AppModule, {
logger: true,
listen: true, // uses the `PORT` / `HOST` env vars, defaults to 3000 / 0.0.0.0
});
console.log('Server running on http://localhost:3000');Features
- ✅ Modern Stage 3 decorators
- ✅ Dependency injection with circular dependency support
- ✅ HTTP method decorators (GET, POST, PUT, PATCH, DELETE)
- ✅ Route parameters, query, and body validation
- ✅ Guards for authentication/authorization
- ✅ Interceptors for request/response transformation
- ✅ Pipes for data transformation and validation
- ✅ Exception filters
- ✅ Module system with imports/exports
- ✅ OpenAPI/Swagger schema support
- ✅ Built-in HTTP exceptions
- ✅ Execution context for middleware
License
MIT