@nestify-js/shared

v0.3.8

Published

Shared utilities and types for nestify-js packages

Downloads

419

Readme

Nestify

npm version npm downloads License: MIT Codacy Badge

中文版本 README.zh.md

⚠️ 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-js

API 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) and JwtGuard are automatically instantiated during apply(). 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's providers, otherwise route registration fails with Cannot find class for token.
  • Where to apply: @UseGuards / @UseInterceptors / @UsePipes / @UseFilters can 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 Injectable too, so @Inject property 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 / @Raw ignore the previous pipe's return value and always extract from the request object. 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 method

Application 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