Skip to content

Commit

Permalink
feat(major restructuring): implement controller and request handler c…
Browse files Browse the repository at this point in the history
…lasses, refactor routing

This update involves implementing new controller and request handler classes, along with refactoring
the routing system. The new controllers are responsible for managing distinct functionalities of the
application, enhancing modularity and clarity in the codebase. Request handlers are designed to
process incoming HTTP requests, ensuring they're directed to the appropriate controllers, while
handling validation and error management. The refactoring of the routing system aligns with these
new structures, improving the efficiency and clarity of URL routing and request processing. Overall,
these changes aim to make the application more robust, maintainable, and scalable, facilitating
easier updates and expansions in the future.
  • Loading branch information
yldrmzffr committed Nov 12, 2023
1 parent 63db545 commit 5af4c20
Show file tree
Hide file tree
Showing 7 changed files with 217 additions and 162 deletions.
36 changes: 36 additions & 0 deletions lib/controller/controller-factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {RouteManager} from '../routing/route-manager';
import * as pathLib from 'path';
import {Route} from '../interfaces';

export type ControllerDecorator = (path?: string) => ClassDecorator;

export class ControllerFactory {
constructor(private routeManager: RouteManager) {}
public Controller: ControllerDecorator = (path = '') => {
return (target: any) => {
Reflect.defineMetadata('path', path, target);

const properties = Object.getOwnPropertyNames(target.prototype);

const routers: (Route | undefined)[] = properties.map(property => {
const routeHandler = target.prototype[property];
const method = Reflect.getMetadata('method', routeHandler);
const url = Reflect.getMetadata('url', routeHandler);

if (!method || !url) return;

const fullPath = pathLib.join(path, url);

return {
method,
url: fullPath,
handler: routeHandler.bind(target.prototype),
} as Route;
});

this.routeManager.addRoutes(routers.filter(Boolean) as Route[]);

return target;
};
};
}
72 changes: 72 additions & 0 deletions lib/handlers/request-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {Request} from '../interfaces';
import {Response} from '../types';
import {RouteManager} from '../routing/route-manager';
import {NotFoundException} from '../exceptions/not-found.exception';
import {getRequestBody, parseQueryParams} from '../utils';
import {MuzuException} from '../exceptions/muzu.exception';
import {BadRequestException} from '../exceptions/bad-request.exception';
export class RequestHandler {
private routeManager: RouteManager;

constructor(routeManager: RouteManager) {
this.routeManager = routeManager;
}

private async sendResponse(
res: Response,
statusCode: number,
body: Object
): Promise<void> {
res.writeHead(statusCode, {'Content-Type': 'application/json'});
res.end(JSON.stringify(body));
console.log('📤 Response', {statusCode, body});
}

public async handleRequest(req: Request, res: Response): Promise<void> {
try {
const {url, method} = req;
const path = url!.split('?')[0];

req.params = parseQueryParams(url!);

const route = await this.routeManager.find(path, method);

if (!route) {
throw new NotFoundException(`Route ${method} ${path} not found`, {
method,
path,
});
}

try {
const body = await getRequestBody(req);
req.body = body;
} catch (error) {
console.log('🚨 Error parsing body', error);
const err = error as MuzuException;
throw new BadRequestException('Error parsing body', err.details);
}

let result: Object = route.handler(req, res);

if (result instanceof Promise) {
result = await result;
}

const statusCode = res.statusCode || 200;
return this.sendResponse(res, statusCode, result);
} catch (error) {
console.log('🚨 Error handling request', error);
const knownError = error as MuzuException;

if (knownError.kind === 'MuzuException') {
return this.sendResponse(res, knownError.status, knownError);
}

return this.sendResponse(res, 500, {
message: '500 Internal Server Error',
stack: knownError.stack,
});
}
}
}
161 changes: 30 additions & 131 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,47 @@
import 'reflect-metadata';
import * as pathLib from 'path';

import {createServer, Server} from 'http';
import {HttpException} from './exceptions/http.exception';

import {Request, Route} from './interfaces';
import {RequestMethod, Response, RouteHandler} from './types';
import {getRequestBody, parseQueryParams} from './utils';
import {MuzuException} from './exceptions/muzu.exception';
import {NotFoundException} from './exceptions/not-found.exception';
import {BadRequestException} from './exceptions/bad-request.exception';
import RouteManager from './route-manager';
import {Request} from './interfaces';
import {Response, RouteHandler} from './types';
import {RouteManager} from './routing/route-manager';
import {RequestHandler} from './handlers/request-handler';
import {
ControllerDecorator,
ControllerFactory,
} from './controller/controller-factory';
import {MethodDecorator, MethodFactory} from './methods/method-factory';

export {Request, Response, RouteHandler, HttpException};

export class MuzuServer {
public readonly server: Server;
public readonly routeManager: RouteManager;
public readonly requestHandler: RequestHandler;
public readonly Controller: ControllerDecorator;
public readonly Get: MethodDecorator;
public readonly Post: MethodDecorator;
public readonly Delete: MethodDecorator;
public readonly Put: MethodDecorator;
public readonly Patch: MethodDecorator;

constructor() {
this.routeManager = new RouteManager();
this.server = createServer(this.handleRequest.bind(this));
const methods = new MethodFactory();

this.Controller = new ControllerFactory(this.routeManager).Controller;
this.requestHandler = new RequestHandler(this.routeManager);

this.server = createServer(
this.requestHandler.handleRequest.bind(this.requestHandler)
);

this.Get = methods.Get;
this.Post = methods.Post;
this.Delete = methods.Delete;
this.Put = methods.Put;
this.Patch = methods.Patch;
}

public listen(port: number, callback?: () => void): void {
Expand All @@ -32,126 +53,4 @@ export class MuzuServer {
public stop(callback?: () => void): void {
this.server.close(callback);
}

private async sendResponse(
res: Response,
statusCode: number,
body: Object
): Promise<void> {
res.writeHead(statusCode, {'Content-Type': 'application/json'});
res.end(JSON.stringify(body));
console.log('📤 Response', {statusCode, body});
}

private async handleRequest(req: Request, res: Response): Promise<void> {
try {
const {url, method} = req;
const path = url!.split('?')[0];

req.params = parseQueryParams(url!);

const route = await this.routeManager.find(path, method);

if (!route) {
throw new NotFoundException(`Route ${method} ${path} not found`, {
method,
path,
});
}

try {
const body = await getRequestBody(req);
req.body = body;
} catch (error) {
console.log('🚨 Error parsing body', error);
const err = error as MuzuException;
throw new BadRequestException('Error parsing body', err.details);
}

let result: Object = route.handler(req, res);

if (result instanceof Promise) {
result = await result;
}

const statusCode = res.statusCode || 200;
return this.sendResponse(res, statusCode, result);
} catch (error) {
console.log('🚨 Error handling request', error);
const knownError = error as MuzuException;

if (knownError.kind === 'MuzuException') {
return this.sendResponse(res, knownError.status, knownError);
}

return this.sendResponse(res, 500, {
message: '500 Internal Server Error',
stack: knownError.stack,
});
}
}

public HttpMethod(method: RequestMethod, url: string) {
return (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) => {
Reflect.defineMetadata(
'url',
`${url}`.toLowerCase(),
target[propertyKey]
);
Reflect.defineMetadata('method', method, target[propertyKey]);
return descriptor;
};
}

Controller = (path = '') => {
return (target: any) => {
Reflect.defineMetadata('path', path, target);

const properties = Object.getOwnPropertyNames(target.prototype);

const routers: (Route | undefined)[] = properties.map(property => {
const routeHandler = target.prototype[property];
const method = Reflect.getMetadata('method', routeHandler);
const url = Reflect.getMetadata('url', routeHandler);

if (!method || !url) return;

const fullPath = pathLib.join(path, url);

return {
method,
url: fullPath,
handler: routeHandler.bind(target.prototype),
} as Route;
});

this.routeManager.addRoutes(routers.filter(Boolean) as Route[]);

return target;
};
};

Get = (url = '/') => {
return this.HttpMethod(RequestMethod.GET, url);
};

Post = (url = '/') => {
return this.HttpMethod(RequestMethod.POST, url);
};

Delete = (url = '/') => {
return this.HttpMethod(RequestMethod.DELETE, url);
};

Put = (url = '/') => {
return this.HttpMethod(RequestMethod.PUT, url);
};

Patch = (url = '/') => {
return this.HttpMethod(RequestMethod.PATCH, url);
};
}
46 changes: 46 additions & 0 deletions lib/methods/method-factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {RequestMethod} from '../types';

type HttpMethodDecorator = (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) => PropertyDescriptor;

export type MethodDecorator = (url?: string) => HttpMethodDecorator;

export class MethodFactory {
public HttpMethod(method: RequestMethod, url: string): HttpMethodDecorator {
return (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) => {
Reflect.defineMetadata(
'url',
`${url}`.toLowerCase(),
target[propertyKey]
);
Reflect.defineMetadata('method', method, target[propertyKey]);
return descriptor;
};
}
Get = (url = '/'): HttpMethodDecorator => {
return this.HttpMethod(RequestMethod.GET, url);
};

Post = (url = '/'): HttpMethodDecorator => {
return this.HttpMethod(RequestMethod.POST, url);
};

Delete = (url = '/'): HttpMethodDecorator => {
return this.HttpMethod(RequestMethod.DELETE, url);
};

Put = (url = '/'): HttpMethodDecorator => {
return this.HttpMethod(RequestMethod.PUT, url);
};

Patch = (url = '/'): HttpMethodDecorator => {
return this.HttpMethod(RequestMethod.PATCH, url);
};
}
30 changes: 0 additions & 30 deletions lib/route-manager.ts

This file was deleted.

Loading

0 comments on commit 5af4c20

Please sign in to comment.