Skip to content

Commit 837e17a

Browse files
committed
feat: add decorators utils
Co-authored-by: Bavithra bavithrarajagopal@gmail.com
1 parent e91b695 commit 837e17a

File tree

6 files changed

+477
-3
lines changed

6 files changed

+477
-3
lines changed

README.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ console.log(isEmail("user@example.com")); // true
5454
- [**String Utilities**](#-string-utilities)
5555
- [**URL Utilities**](#-url-utilities)
5656
- [**Validate Utilities**](#-validate-utilities)
57+
- [**Decorators Utilities**](#decorators-utilities)
5758

5859
---
5960

@@ -408,6 +409,75 @@ A comprehensive suite of string/format validators for safe input and API checks.
408409

409410
---
410411

412+
# 📧 Decorators Utilities
413+
414+
## Available Decorators
415+
416+
- `@Controller(basePath: string)` – Class decorator to set the base route.
417+
- `@Get(path)`, `@Post(path)`, `@Put(path)`, `@Patch(path)`, `@Delete(path)`, `@Options(path)`, `@Head(path)`, `@Trace(path)`, `@Connect(path)` – Method decorators for HTTP verbs.
418+
- `@Use(...middlewares)` – Attach Express-style middleware to a route handler.
419+
- `@Query(key?)`, `@Param(key?)`, `@Body(key?)`, `@Req()`, `@Res()` – Parameter decorators for extracting request data.
420+
- `@HttpCode(status)` – Set custom HTTP status code for the response.
421+
- `@Header(name, value)` – Set custom response headers.
422+
- `@Before(fn)`, `@After(fn)` – Register before/after hooks for a route handler.
423+
424+
## Example Usage
425+
426+
```typescript
427+
import {
428+
Controller, Get, Post, Use, Query, Param, Body, Req, Res,
429+
HttpCode, Header, Before, After, registerControllers, Request, Response, NextFunction
430+
} from './src/utils/decorators.utils';
431+
432+
// Example middleware
433+
function logMiddleware(req: Request, res: Response, next: NextFunction) {
434+
console.log('Request:', req.method, req.url);
435+
next();
436+
}
437+
438+
// Example before/after hooks
439+
function beforeHook(req: Request, res: Response) {
440+
console.log('Before handler');
441+
}
442+
function afterHook(req: Request, res: Response, result: any) {
443+
console.log('After handler', result);
444+
}
445+
446+
@Controller('/api')
447+
class ExampleController {
448+
@Get('/items/:id')
449+
@Use(logMiddleware)
450+
@HttpCode(200)
451+
@Header('X-Example', 'yes')
452+
@Before(beforeHook)
453+
@After(afterHook)
454+
getItem(
455+
@Query('q') q: string,
456+
@Param('id') id: string,
457+
@Body('name') name: string,
458+
@Req() req: Request,
459+
@Res() res: Response
460+
) {
461+
return { q, id, name };
462+
}
463+
464+
@Post('/items')
465+
createItem(@Body() body: any) {
466+
return { created: true, ...body };
467+
}
468+
}
469+
470+
// Register controllers with your router (Express-like)
471+
const router = /* your router instance */;
472+
registerControllers(router, [ExampleController]);
473+
```
474+
475+
## Notes
476+
477+
- Decorated methods **must** use standard method syntax, not arrow functions or property initializers.
478+
- All parameter decorators (`@Query`, `@Param`, etc.) are optional and can be used in any order.
479+
- `registerControllers(router, controllers)` will register all routes and apply middlewares, hooks, status codes, and headers as defined.
480+
411481
## 🏁 Usage
412482

413483
Import only what you need:

package-lock.json

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@
4747
],
4848
"dependencies": {
4949
"abort-controller": "^3.0.0",
50-
"pino": "^9.7.0"
50+
"pino": "^9.7.0",
51+
"reflect-metadata": "^0.2.2"
5152
},
5253
"devDependencies": {
5354
"@types/jest": "^30.0.0",

src/utils/decorators.utils.ts

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
/* eslint-disable @typescript-eslint/no-unused-vars */
2+
3+
import "reflect-metadata";
4+
5+
// --- Sample Express interfaces for type safety ---
6+
export interface Request {
7+
query: any;
8+
params: any;
9+
body?: any;
10+
[key: string]: any;
11+
}
12+
export interface Response {
13+
json: (body: any) => void;
14+
headersSent: boolean;
15+
[key: string]: any;
16+
}
17+
export type NextFunction = (err?: any) => void;
18+
export type RequestHandler = (
19+
req: Request,
20+
res: Response,
21+
next: NextFunction,
22+
) => any;
23+
export interface Router {
24+
[method: string]: (path: string, ...handlers: RequestHandler[]) => void;
25+
}
26+
// --- End sample Express interfaces ---
27+
28+
const ROUTES_KEY = Symbol("routes");
29+
const MIDDLEWARE_KEY = Symbol("middlewares");
30+
const PARAMS_KEY = Symbol("params");
31+
32+
export type HttpMethod =
33+
| "get"
34+
| "post"
35+
| "put"
36+
| "patch"
37+
| "delete"
38+
| "options"
39+
| "head"
40+
| "trace"
41+
| "connect";
42+
43+
interface RouteDefinition {
44+
path: string;
45+
method: HttpMethod;
46+
handlerName: string;
47+
}
48+
49+
interface ParamDefinition {
50+
index: number;
51+
type: "query" | "param" | "body" | "req" | "res";
52+
key?: string;
53+
}
54+
55+
// ---- Route Decorators ----
56+
function createRouteDecorator(method: HttpMethod) {
57+
return (path: string): MethodDecorator => {
58+
return (target, propertyKey, descriptor) => {
59+
const routes: RouteDefinition[] =
60+
Reflect.getMetadata(ROUTES_KEY, target.constructor) || [];
61+
routes.push({
62+
path,
63+
method,
64+
handlerName: propertyKey as string,
65+
});
66+
Reflect.defineMetadata(ROUTES_KEY, routes, target.constructor);
67+
};
68+
};
69+
}
70+
71+
export const Get = createRouteDecorator("get");
72+
export const Post = createRouteDecorator("post");
73+
export const Put = createRouteDecorator("put");
74+
export const Patch = createRouteDecorator("patch");
75+
export const Delete = createRouteDecorator("delete");
76+
export const Options = createRouteDecorator("options");
77+
export const Head = createRouteDecorator("head");
78+
export const Trace = createRouteDecorator("trace");
79+
export const Connect = createRouteDecorator("connect");
80+
81+
// ---- Controller Decorator ----
82+
export function Controller(basePath: string): ClassDecorator {
83+
return (target) => {
84+
Reflect.defineMetadata("basePath", basePath, target);
85+
};
86+
}
87+
88+
// ---- Middleware Decorator ----
89+
export function Use(...middlewares: RequestHandler[]): MethodDecorator {
90+
return (target, propertyKey, descriptor) => {
91+
const existing: RequestHandler[] =
92+
Reflect.getMetadata(MIDDLEWARE_KEY, target, propertyKey as string) || [];
93+
Reflect.defineMetadata(
94+
MIDDLEWARE_KEY,
95+
[...existing, ...middlewares],
96+
target,
97+
propertyKey as string,
98+
);
99+
};
100+
}
101+
102+
// ---- Parameter Decorators ----
103+
function createParamDecorator(type: ParamDefinition["type"], key?: string) {
104+
return (paramKey?: string): ParameterDecorator => {
105+
return (target, propertyKey, parameterIndex) => {
106+
const params: ParamDefinition[] =
107+
Reflect.getMetadata(PARAMS_KEY, target, propertyKey as string) || [];
108+
// Ensure parameters are ordered by index
109+
params.push({ index: parameterIndex, type, key: paramKey || key });
110+
params.sort((a, b) => a.index - b.index);
111+
Reflect.defineMetadata(PARAMS_KEY, params, target, propertyKey as string);
112+
};
113+
};
114+
}
115+
116+
export const Query = createParamDecorator("query");
117+
export const Param = createParamDecorator("param");
118+
export const Body = createParamDecorator("body");
119+
export const Req = createParamDecorator("req");
120+
export const Res = createParamDecorator("res");
121+
122+
// Custom HTTP status code decorator
123+
const HTTP_CODE_KEY = Symbol("httpCode");
124+
export function HttpCode(status: number): MethodDecorator {
125+
return (target, propertyKey, descriptor) => {
126+
Reflect.defineMetadata(
127+
HTTP_CODE_KEY,
128+
status,
129+
target,
130+
propertyKey as string,
131+
);
132+
};
133+
}
134+
135+
// Custom header decorator
136+
const HEADER_KEY = Symbol("headers");
137+
export function Header(name: string, value: string): MethodDecorator {
138+
return (target, propertyKey, descriptor) => {
139+
const headers: Record<string, string> =
140+
Reflect.getMetadata(HEADER_KEY, target, propertyKey as string) || {};
141+
headers[name] = value;
142+
Reflect.defineMetadata(HEADER_KEY, headers, target, propertyKey as string);
143+
};
144+
}
145+
146+
// Before/After hooks (not Express middleware, but can be used for logging, etc.)
147+
const BEFORE_KEY = Symbol("before");
148+
const AFTER_KEY = Symbol("after");
149+
export function Before(fn: Function): MethodDecorator {
150+
return (target, propertyKey, descriptor) => {
151+
const hooks: Function[] =
152+
Reflect.getMetadata(BEFORE_KEY, target, propertyKey as string) || [];
153+
hooks.push(fn);
154+
Reflect.defineMetadata(BEFORE_KEY, hooks, target, propertyKey as string);
155+
};
156+
}
157+
export function After(fn: Function): MethodDecorator {
158+
return (target, propertyKey, descriptor) => {
159+
const hooks: Function[] =
160+
Reflect.getMetadata(AFTER_KEY, target, propertyKey as string) || [];
161+
hooks.push(fn);
162+
Reflect.defineMetadata(AFTER_KEY, hooks, target, propertyKey as string);
163+
};
164+
}
165+
166+
// ---- Register Controllers ----
167+
export function registerControllers(router: Router, controllers: any[]) {
168+
controllers.forEach((ControllerClass) => {
169+
const instance = new ControllerClass();
170+
const basePath: string =
171+
Reflect.getMetadata("basePath", ControllerClass) || "";
172+
const routes: RouteDefinition[] =
173+
Reflect.getMetadata(ROUTES_KEY, ControllerClass) || [];
174+
175+
routes.forEach(({ path, method, handlerName }) => {
176+
const middlewares: RequestHandler[] =
177+
Reflect.getMetadata(MIDDLEWARE_KEY, instance, handlerName) || [];
178+
const params: ParamDefinition[] =
179+
Reflect.getMetadata(PARAMS_KEY, instance, handlerName) || [];
180+
181+
const httpCode: number | undefined = Reflect.getMetadata(
182+
HTTP_CODE_KEY,
183+
instance,
184+
handlerName,
185+
);
186+
const headers: Record<string, string> =
187+
Reflect.getMetadata(HEADER_KEY, instance, handlerName) || {};
188+
const beforeHooks: Function[] =
189+
Reflect.getMetadata(BEFORE_KEY, instance, handlerName) || [];
190+
const afterHooks: Function[] =
191+
Reflect.getMetadata(AFTER_KEY, instance, handlerName) || [];
192+
193+
const handler: RequestHandler = async (req, res, next) => {
194+
try {
195+
for (const fn of beforeHooks) await fn(req, res);
196+
const args: any[] = [];
197+
if (params.length) {
198+
params.forEach(({ index, type, key }) => {
199+
switch (type) {
200+
case "query":
201+
args[index] = key ? req.query[key] : req.query;
202+
break;
203+
case "param":
204+
args[index] = key ? req.params[key] : req.params;
205+
break;
206+
case "body":
207+
args[index] = key ? req.body?.[key] : req.body;
208+
break;
209+
case "req":
210+
args[index] = req;
211+
break;
212+
case "res":
213+
args[index] = res;
214+
break;
215+
}
216+
});
217+
}
218+
const result = (instance as any)[handlerName](...args);
219+
// Support both sync and async handlers
220+
const awaited = result instanceof Promise ? await result : result;
221+
if (!res.headersSent && typeof awaited !== "undefined") {
222+
if (httpCode) res.status?.(httpCode);
223+
for (const [k, v] of Object.entries(headers)) res.set?.(k, v);
224+
res.json(awaited);
225+
}
226+
for (const fn of afterHooks) await fn(req, res, awaited);
227+
} catch (err) {
228+
next(err);
229+
}
230+
};
231+
232+
(router as any)[method](basePath + path, ...middlewares, handler);
233+
});
234+
});
235+
}

0 commit comments

Comments
 (0)