Skip to main content
Glama
decorators.ts5.87 kB
import "reflect-metadata"; import type { Application, NextFunction, Request, RequestHandler, Response, } from "express"; import type { z } from "zod"; // Metadata keys for storing decorator information const ROUTES_KEY = Symbol("routes"); const MIDDLEWARES_KEY = Symbol("middlewares"); const SCHEMA_KEY = Symbol("schema"); // Route metadata interface export interface RouteMetadata { method: "get" | "post" | "put" | "delete" | "patch"; path: string; propertyKey: string; descriptor: PropertyDescriptor; middlewares?: RequestHandler[]; schema?: { body?: z.ZodSchema; query?: z.ZodSchema; params?: z.ZodSchema; response?: z.ZodSchema; }; description?: string; summary?: string; tags?: string[]; operationId?: string; } // Controller decorator export function Controller(basePath = "") { return <T extends new (...args: unknown[]) => unknown>(targetClass: T) => { Reflect.defineMetadata("basePath", basePath, targetClass); return targetClass; }; } // HTTP method decorators function createMethodDecorator(method: RouteMetadata["method"]) { return (path = ""): MethodDecorator => ( target: object, propertyKey: string | symbol, descriptor: PropertyDescriptor, ) => { const routes: RouteMetadata[] = Reflect.getMetadata(ROUTES_KEY, target) || []; const middlewares = Reflect.getMetadata(MIDDLEWARES_KEY, target, propertyKey) || []; const schema = Reflect.getMetadata(SCHEMA_KEY, target, propertyKey); routes.push({ method, path, propertyKey: String(propertyKey), descriptor, middlewares, schema, }); Reflect.defineMetadata(ROUTES_KEY, routes, target); }; } export const Get = createMethodDecorator("get"); export const Post = createMethodDecorator("post"); export const Put = createMethodDecorator("put"); export const Delete = createMethodDecorator("delete"); export const Patch = createMethodDecorator("patch"); // Middleware decorator export function UseMiddleware( ...middlewares: RequestHandler[] ): MethodDecorator { return ( target: object, propertyKey: string | symbol, descriptor: PropertyDescriptor, ) => { const existingMiddlewares = Reflect.getMetadata(MIDDLEWARES_KEY, target, propertyKey) || []; Reflect.defineMetadata( MIDDLEWARES_KEY, [...existingMiddlewares, ...middlewares], target, propertyKey, ); return descriptor; }; } // Schema validation decorator export function Schema(schema: { body?: z.ZodSchema; query?: z.ZodSchema; params?: z.ZodSchema; response?: z.ZodSchema; }): MethodDecorator { return ( target: object, propertyKey: string | symbol, descriptor: PropertyDescriptor, ) => { Reflect.defineMetadata(SCHEMA_KEY, schema, target, propertyKey); // Create validation middleware const originalMethod = descriptor.value; descriptor.value = async function ( req: Request, res: Response, next: NextFunction, ) { try { // Validate request body if (schema.body) { const result = schema.body.safeParse(req.body); if (!result.success) { return res.status(400).json({ error: "Validation error", details: result.error.format(), }); } req.body = result.data; } // Validate query parameters if (schema.query) { const result = schema.query.safeParse(req.query); if (!result.success) { return res.status(400).json({ error: "Query validation error", details: result.error.format(), }); } req.query = result.data; } // Validate path parameters if (schema.params) { const result = schema.params.safeParse(req.params); if (!result.success) { return res.status(400).json({ error: "Params validation error", details: result.error.format(), }); } req.params = result.data; } // Call original method const response = await originalMethod.call(this, req, res, next); // Validate response if schema is provided if (schema.response && response !== undefined) { const result = schema.response.safeParse(response); if (!result.success) { console.error("Response validation error:", result.error.format()); return res.status(500).json({ error: "Internal server error", message: "Response validation failed", }); } return res.json(result.data); } return response; } catch (error) { next(error); } }; return descriptor; }; } // API documentation decorators export function ApiOperation(options: { summary?: string; description?: string; tags?: string[]; operationId?: string; }): MethodDecorator { return ( target: object, propertyKey: string | symbol, descriptor: PropertyDescriptor, ) => { const routes: RouteMetadata[] = Reflect.getMetadata(ROUTES_KEY, target) || []; const route = routes.find((r) => r.propertyKey === String(propertyKey)); if (route) { Object.assign(route, options); } return descriptor; }; } // Helper to extract routes from a controller class export function getControllerMetadata(controller: object): { basePath: string; routes: RouteMetadata[]; } { const basePath = Reflect.getMetadata("basePath", controller.constructor) || ""; const routes = Reflect.getMetadata(ROUTES_KEY, controller) || []; return { basePath, routes }; } // Helper to register controller with Express app export function registerController(app: Application, controller: object) { const { basePath, routes } = getControllerMetadata(controller); for (const route of routes) { const fullPath = `${basePath}${route.path}`; const handler = route.descriptor.value.bind(controller); const middlewares = route.middlewares || []; // Register route with Express (app as unknown as { [key: string]: (...args: unknown[]) => unknown })[ route.method ](fullPath, ...middlewares, handler); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/bowen31337/expressjs_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server