ts5deco-express-controller
v0.1.0
Published
TypeScript 5 Modern Decorator Express Controller Framework
Maintainers
Readme
ts5deco Express Controller Framework
Modern TypeScript 5 Decorator-based Express Controller Framework
Features
- 🎯 Modern Decorators: Uses TypeScript 5 modern decorators (not legacy experimental decorators)
- 🚀 Express Integration: Seamless integration with Express.js
- 🔧 Type Safety: Full TypeScript support with strong typing
- 🎨 Clean Architecture: Decorator-based controller definition
- 🛡️ Middleware Support: Built-in middleware support
- 🔄 Auto Registration: Automatic route registration
- 🚫 No reflect-metadata: Uses native WeakMap-based metadata storage
- ✨ Type-Safe Responses: Generic response classes with compile-time type checking
- 🎭 Response System: JsonResponse, TextResponse, NoContentResponse, RedirectResponse, and FileResponse
- 📋 OpenAPI Integration: Generate TypeScript types from OpenAPI/Swagger specifications
- 🔒 OpenAPI Type Safety: Compile-time response type validation with OpenAPI specs
- 🛠️ CLI Tools: Built-in CLI for project initialization and type generation
- 🔗 Programmatic API: Full programmatic control over type generation
Installation
npm install ts5deco-express-controller
# or
yarn add ts5deco-express-controller
# or
pnpm add ts5deco-express-controllerQuick Start
1. Configure TypeScript
Make sure your tsconfig.json includes:
{
"compilerOptions": {
"experimentalDecorators": false,
"emitDecoratorMetadata": false,
"target": "ES2022",
"lib": ["ES2022"],
"module": "commonjs"
}
}2. Create a Controller
import express from 'express';
import { Controller, Get, Post } from 'ts5deco-express-controller';
interface CreateUserDto {
name: string;
email: string;
}
@Controller('/users')
export class UserController {
@Get()
async getAllUsers(req: express.Request, res: express.Response, next: express.NextFunction) {
const page = req.query.page as string || '1';
// Get all users with optional pagination
return { users: [], page };
}
@Get('/:id')
async getUserById(req: express.Request, res: express.Response, next: express.NextFunction) {
const id = req.params.id;
// Get user by ID
return { id, name: 'John Doe', email: '[email protected]' };
}
@Post()
async createUser(req: express.Request, res: express.Response, next: express.NextFunction) {
const userData = req.body as CreateUserDto;
// Create new user
return { id: '123', ...userData };
}
}3. Register Controllers
import express from 'express';
import { registerControllers } from 'ts5deco-express-controller';
import { UserController } from './controllers/user.controller';
const app = express();
// Middleware
app.use(express.json());
// Register controllers
registerControllers(app, [UserController], '/api');
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});API Reference
Decorators
@Controller(path?, options?)
Defines a controller class.
@Controller('/api/users')
export class UserController {
// ...
}
// With middleware
@Controller({
path: '/api/users',
middlewares: [authMiddleware]
})
export class UserController {
// ...
}Route Decorators
@Get(path?, options?)- GET requests@Post(path?, options?)- POST requests@Put(path?, options?)- PUT requests@Delete(path?, options?)- DELETE requests@Patch(path?, options?)- PATCH requests@Head(path?, options?)- HEAD requests@Options(path?, options?)- OPTIONS requests@All(path?, options?)- All HTTP methods
All route handlers receive Express's standard req, res, and next parameters:
@Get('/profile')
async getProfile(req: express.Request, res: express.Response, next: express.NextFunction) {
// Access query parameters: req.query
// Access URL parameters: req.params
// Access request body: req.body
// Access headers: req.headers
return { profile: 'data' };
}
@Post({ path: '/users', middlewares: [validateUser] })
async createUser(req: express.Request, res: express.Response, next: express.NextFunction) {
const userData = req.body;
return { user: userData };
}Middleware Decorators
@Use(...middlewares)- Apply middleware to route@Authenticated(middleware)- Authentication middleware@Authorized(middleware)- Authorization middleware@Validated(middleware)- Validation middleware
@Use(loggingMiddleware, rateLimitMiddleware)
@Get('/protected')
async getProtectedData() {
// ...
}
@Authenticated(jwtAuthMiddleware)
@Get('/profile')
async getProfile() {
// ...
}Functions
createRouter(controllers)
Creates an Express router from controllers.
import { createRouter } from 'ts5deco-express-controller';
const router = createRouter([UserController, PostController]);
app.use('/api', router);registerControllers(app, controllers, basePath?)
Registers controllers directly to an Express app.
import { registerControllers } from 'ts5deco-express-controller';
registerControllers(app, [UserController, PostController], '/api');registerController(router, controller)
Registers a single controller to a router.
import { registerController } from 'ts5deco-express-controller';
const router = express.Router();
registerController(router, UserController);Response System
The framework provides a powerful type-safe response system that eliminates repetitive res.status().json() boilerplate code.
Basic Usage
import {
JsonResponse,
JsonResponses,
TextResponse,
TextResponses,
NoContentResponse,
RedirectResponse,
FileResponse
} from 'ts5deco-express-controller';
@Controller('/api/users')
export class UserController {
// Type-safe JSON responses
@Get('/')
async getUsers(): Promise<JsonResponse<User[], 200>> {
const users = await this.userService.getUsers();
return new JsonResponse<User[], 200>(200, users);
}
@Get('/:id')
async getUserById(req: Request): Promise<JsonResponse<User, 200> | JsonResponse<ErrorResponse, 404>> {
const user = await this.userService.findById(req.params.id);
if (!user) {
return JsonResponses.notFound<ErrorResponse>({
error: 'User not found',
message: 'The requested user does not exist'
});
}
return JsonResponses.ok<User>(user);
}
@Post('/')
async createUser(req: Request): Promise<JsonResponse<User, 201>> {
const user = await this.userService.create(req.body);
return JsonResponses.created<User>(user);
}
@Delete('/:id')
async deleteUser(): Promise<NoContentResponse> {
await this.userService.delete(req.params.id);
return new NoContentResponse(); // 204 No Content
}
// Text responses with literal types
@Get('/health')
async healthCheck(): Promise<TextResponse<'healthy', 200>> {
return new TextResponse<'healthy', 200>(200, 'healthy');
}
// Redirects
@Get('/old-endpoint')
async oldEndpoint(): Promise<RedirectResponse> {
return new RedirectResponse('/api/users', 301); // Permanent redirect
}
// File downloads
@Get('/export')
async exportUsers(): Promise<FileResponse> {
return new FileResponse('/tmp/users.csv', 'users-export.csv', true);
}
}Response Types
JsonResponse<TData, TStatus>
Type-safe JSON responses with generics for data and status code:
// Direct usage
return new JsonResponse<User, 200>(200, user);
// Convenience methods
return JsonResponses.ok<User>(user); // 200 OK
return JsonResponses.created<User>(user); // 201 Created
return JsonResponses.badRequest<Error>(error); // 400 Bad Request
return JsonResponses.notFound<Error>(error); // 404 Not FoundTextResponse<TText, TStatus>
Type-safe text responses:
// With literal types
return new TextResponse<'OK', 200>(200, 'OK');
// With template literal types
return new TextResponse<`v${string}`, 200>(200, 'v1.2.3');
// With union types
return new TextResponse<'running' | 'stopped', 200>(200, 'running');
// Convenience methods
return TextResponses.ok('Service is healthy');Other Response Types
// 204 No Content
return new NoContentResponse();
// Redirects
return new RedirectResponse('/new-path', 302);
return RedirectResponses.permanent('/new-path'); // 301
return RedirectResponses.temporary('/new-path'); // 302
// File responses
return new FileResponse('/path/to/file.pdf', 'document.pdf');
return FileResponses.attachment('/path/to/file.zip', 'archive.zip');Benefits
- Type Safety: Compile-time checking of response data and status codes
- Clean Code: No more repetitive
res.status().json()calls - Consistency: Standardized response handling across your application
- IDE Support: Full IntelliSense and auto-completion
- Backward Compatible: Works alongside existing Express response methods
For complete documentation, see Response System Documentation.
OpenAPI Integration
The framework supports generating TypeScript types from OpenAPI/Swagger specifications, ensuring type safety between your API documentation and implementation.
Quick Start with OpenAPI
1. Initialize a New Project
npx ts5deco-express-controller init --dir ./my-api-project2. Generate Types from OpenAPI Spec
npx ts5deco-express-controller generate --input ./api/openapi.yaml --output ./src/types/generated3. Use Generated Types in Controllers
import { Controller, Get, Post, JsonResponses } from 'ts5deco-express-controller';
import type { User, ErrorResponse, CreateUserRequest } from './types/api';
@Controller('/api/users')
export class UserController {
@Get('/:id')
async getUserById(
req: Request,
res: Response,
next: NextFunction
): Promise<JsonResponse<User, 200> | JsonResponse<ErrorResponse, 404>> {
const user = await this.userService.findById(req.params.id);
if (!user) {
return JsonResponses.notFound<ErrorResponse>({
error: 'NOT_FOUND',
message: 'User not found'
});
}
return JsonResponses.ok<User>(user);
}
@Post('/')
async createUser(
req: Request,
res: Response,
next: NextFunction
): Promise<JsonResponse<User, 201>> {
const userData = req.body as CreateUserRequest;
const user = await this.userService.create(userData);
return JsonResponses.created<User>(user);
}
}CLI Commands
Project Initialization
# Initialize new project with OpenAPI setup
npx ts5deco-express-controller init --dir ./my-projectType Generation
# Generate types from OpenAPI spec
npx ts5deco-express-controller generate --input ./api/openapi.yaml --output ./src/types/generated
# With custom helper files
npx ts5deco-express-controller generate \
--input ./api/openapi.yaml \
--output ./src/types/generated \
--api-types ./src/types/api.ts \
--utils ./src/types/openapi-utils.tsProgrammatic API
import { generateTypes, initProject } from 'ts5deco-express-controller';
// Initialize project
await initProject('./my-project');
// Generate types
await generateTypes({
input: './api/openapi.yaml',
outputDir: './src/types/generated',
apiTypesPath: './src/types/api.ts',
utilsPath: './src/types/openapi-utils.ts',
});Benefits
- Single Source of Truth: Your OpenAPI spec drives both documentation and types
- Compile-time Safety: Catch type mismatches before runtime
- Auto-completion: Full IDE support with generated types
- Consistency: Ensure API implementation matches specification
- Zero Runtime Overhead: Pure compile-time type checking
For complete documentation, see OpenAPI Integration Guide.
OpenAPI Type Safety
The framework provides compile-time type safety by connecting OpenAPI specifications with route decorators. This ensures your API implementation exactly matches your OpenAPI spec.
Basic Usage
import { createTypedRoutes } from 'ts5deco-express-controller';
import type { paths } from './types/generated/api'; // Generated from OpenAPI
import type { ApiResponse } from 'ts5deco-express-controller';
// Create type-safe route decorators
const TypedRoutes = createTypedRoutes<paths>();
@Controller('/api')
export class UserController {
// ✅ Type-safe: Only allows responses defined in OpenAPI spec
@TypedRoutes.Get('/users/{id}')
async getUserById(): Promise<ApiResponse<paths, '/users/{id}', 'get'>> {
return JsonResponse.ok(user); // ✅ 200 response (defined in spec)
return JsonResponse.notFound(err); // ✅ 404 response (defined in spec)
// return JsonResponse.created(user); // ❌ Compile error: 201 not in spec
}
@TypedRoutes.Post('/users')
async createUser(): Promise<ApiResponse<paths, '/users', 'post'>> {
return JsonResponse.created(user); // ✅ 201 response (defined in spec)
return JsonResponse.badRequest(err); // ✅ 400 response (defined in spec)
// return JsonResponse.ok(user); // ❌ Compile error: 200 not in spec
}
}Setup Steps
- Generate types from OpenAPI spec:
npx openapi-typescript ./openapi.yaml -o ./src/types/generated/api.d.ts- Use type-safe decorators:
const TypedRoutes = createTypedRoutes<paths>();- Define type-safe methods:
@TypedRoutes.Get('/users/{id}')
async getUserById(): Promise<ApiResponse<paths, '/users/{id}', 'get'>> {
// Implementation must match OpenAPI spec
}Benefits
- Enhanced Developer Experience: IDE auto-completion and type hints
- OpenAPI Sync: Clear connection between API implementation and specification
- Type Guidance: Explicit guidance on allowed status codes and response types
- Code Consistency: Ensures consistent API implementation patterns
- Zero Runtime Cost: Type checking happens only at compile time
Note: Due to TypeScript's structural typing, this system provides enhanced developer experience and code guidance rather than complete compile-time error prevention. The focus is on improving code quality and maintaining consistency with OpenAPI specifications.
Path Conversion
OpenAPI paths (/users/{id}) are automatically converted to Express paths (/users/:id):
@TypedRoutes.Get('/users/{id}') // OpenAPI path
// Becomes: app.get('/users/:id', ...) // Express routeMigration Strategy
You can gradually migrate from regular decorators to type-safe ones:
export class UserController {
// Existing code - no type checking
@Get('/users/legacy')
async legacyEndpoint() {
return JsonResponse.ok(data); // Any status code allowed
}
// New code - type-safe
@TypedRoutes.Get('/users')
async typedEndpoint(): Promise<ApiResponse<paths, '/users', 'get'>> {
return JsonResponse.ok(data); // Only spec-defined responses allowed
}
}For complete examples and advanced usage, see Typed Routes Guide.
Examples
Complete Example
import express from 'express';
import {
Controller,
Get,
Post,
Put,
Delete,
Use,
Authenticated,
registerControllers
} from 'ts5deco-express-controller';
// Middleware functions
const loggingMiddleware = (req: express.Request, res: express.Response, next: express.NextFunction) => {
console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`);
next();
};
const authMiddleware = (req: express.Request, res: express.Response, next: express.NextFunction) => {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ error: 'Unauthorized' });
}
// Simple token validation (use JWT in real applications)
if (token !== 'Bearer valid-token') {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
// DTOs
interface CreateUserDto {
name: string;
email: string;
}
interface UpdateUserDto {
name?: string;
email?: string;
}
// Controller
@Controller({ path: '/users', middlewares: [loggingMiddleware] })
export class UserController {
@Get()
async getUsers(req: express.Request, res: express.Response, next: express.NextFunction) {
const page = req.query.page as string || '1';
const limit = req.query.limit as string || '10';
return {
users: [
{ id: '1', name: 'John Doe', email: '[email protected]' },
{ id: '2', name: 'Jane Smith', email: '[email protected]' }
],
pagination: { page: parseInt(page), limit: parseInt(limit) }
};
}
@Get('/:id')
async getUser(req: express.Request, res: express.Response, next: express.NextFunction) {
const id = req.params.id;
if (id === '1') {
return { id: '1', name: 'John Doe', email: '[email protected]' };
}
res.status(404);
return { error: 'User not found' };
}
@Post()
async createUser(req: express.Request, res: express.Response, next: express.NextFunction) {
const userData = req.body as CreateUserDto;
const newUser = {
id: Date.now().toString(),
...userData,
createdAt: new Date().toISOString()
};
return { user: newUser, message: 'User created successfully' };
}
@Put('/:id')
@Authenticated(authMiddleware)
async updateUser(req: express.Request, res: express.Response, next: express.NextFunction) {
const id = req.params.id;
const userData = req.body as UpdateUserDto;
const updatedUser = {
id,
...userData,
updatedAt: new Date().toISOString()
};
return { user: updatedUser, message: 'User updated successfully' };
}
@Delete('/:id')
@Authenticated(authMiddleware)
async deleteUser(req: express.Request, res: express.Response, next: express.NextFunction) {
const id = req.params.id;
return { message: 'User deleted successfully', id };
}
}
// App setup
const app = express();
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Register controllers
registerControllers(app, [UserController], '/api');
// Basic route
app.get('/', (req, res) => {
res.json({
message: 'ts5deco Express Controller Framework',
endpoints: {
users: '/api/users'
}
});
});
// Error handling
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
console.error('Error:', err);
res.status(500).json({ error: 'Internal Server Error' });
});
// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
console.log('Available endpoints:');
console.log('- GET /api/users');
console.log('- GET /api/users/:id');
console.log('- POST /api/users');
console.log('- PUT /api/users/:id (requires auth)');
console.log('- DELETE /api/users/:id (requires auth)');
console.log('');
console.log('For protected routes, use header: Authorization: Bearer valid-token');
});Requirements
- Node.js 16+
- TypeScript 5.0+
- Express 4.18+
License
MIT License - see the LICENSE file for details.
