arrow-express
Version:
Library to bootstrap express applications with zero configuration
185 lines (160 loc) • 5.86 kB
text/typescript
import Express from "express";
import { ControllerConfiguration } from "../controller/controller";
import { RouteConfigurator } from "../route/route";
import { RequestError } from "../error/request.error";
import { ConfigurationError } from "../error/configuration.error";
export class AppConfigurator {
private _prefix = "";
private readonly _express: Express.Application;
private readonly _controllers: ControllerConfiguration<any>[] = [];
private readonly logRequests: boolean;
private _configured: boolean;
/**
* Create AppConfigurator
* @param expressApplication - express application
* @param logRequests - flag if requests should be logged, true by default
*/
constructor(expressApplication: Express.Application, logRequests = true) {
this._express = expressApplication;
this._configured = false;
this.logRequests = logRequests;
}
/**
* Starts application, register controllers routes in express app
* and connect to configured port.
* @param printConfiguration - print express application routes enabled by default.
*/
configure(printConfiguration = true): void {
if (this._configured) {
throw new ConfigurationError("Cannot configure application multiple times");
} else {
this._configured = true;
}
this.configureControllers();
if (printConfiguration) {
this.printExpressConfig();
}
}
/**
* Register prefix for all paths in application
* @param prefix - prefix string eg: 'api'
*/
prefix(prefix: string): AppConfigurator {
this._prefix = prefix;
return this;
}
/**
* Register controller in application.
* @param controller - registered controller
*/
registerController(controller: ControllerConfiguration<any, any>): AppConfigurator {
this._controllers.push(controller);
return this;
}
/**
* Register list of controllers in application.
* @param controllers - controllers to register
*/
registerControllers(...controllers: ControllerConfiguration<any, any>[]): AppConfigurator {
controllers.forEach(controller => this.registerController(controller));
return this;
}
// PRIVATE
private printExpressConfig() {
console.log("Routes registered by Express server:");
this.getExpressRoutesAsStrings().forEach(route => console.log(route));
}
private configureControllers() {
this._controllers.forEach(controller => this.configureController(controller));
}
private configureController(controller: ControllerConfiguration, controllersChain?: ControllerConfiguration[]) {
if (!controllersChain) {
controllersChain = [controller];
} else {
controllersChain = [...controllersChain, controller];
}
controller.getControllers().forEach(subController => {
this.configureController(subController, controllersChain);
});
controller.getRoutes().forEach(route => {
this.registerRouteInExpress(controllersChain, route);
});
}
private registerRouteInExpress(controllersChain: ControllerConfiguration[], route: RouteConfigurator) {
const controllersPrefix = controllersChain.reduce(
(prefixAcc, controller) => AppConfigurator.getRoutePath(prefixAcc, controller.getPrefix()),
this._prefix || ""
);
const routePath = AppConfigurator.getRoutePath(controllersPrefix, route.getPath());
if (!route.getMethod()) {
throw new ConfigurationError(`Route ${routePath} has no method specified`);
}
this._express[route.getMethod()](`/${routePath}`, this.createApplicationRequestHandler(route, controllersChain));
}
private createApplicationRequestHandler(
route: RouteConfigurator,
controllersChain: ControllerConfiguration[]
): Express.RequestHandler {
return async (req: Express.Request, res: Express.Response) => {
try {
let context: unknown;
for (const controller of controllersChain) {
const newContextValue = await controller.getHandler()?.(req, res, context);
context = newContextValue ? newContextValue : context;
}
const response = await route.getRequestHandler()(req, res, context);
if (AppConfigurator.canSendResponse(res)) {
if (!res.statusCode) {
res.status(200);
}
res.send(response);
}
} catch (error) {
if (AppConfigurator.canSendResponse(res)) {
if (error instanceof RequestError) {
res.status(error.httpCode || 500).send(error.response || "Internal error");
} else {
res.status(500).send("Internal error");
}
}
} finally {
this.logRequest(req, res);
}
};
}
private logRequest(req: Express.Request, res: Express.Response) {
if (this.logRequests) {
console.log(`Request ${req.method}:${req.path} Response status: ${res.statusCode}`);
}
}
private getExpressRoutesAsStrings() {
return this._express._router.stack.filter(r => r.route).map(AppConfigurator.expressRouteAsString);
}
// STATIC
private static expressRouteAsString(r) {
return `${Object.keys(r.route.methods)[0].toUpperCase()}:${r.route?.path}`;
}
/**
* Get final route path
* @param paths - array of paths
* @private
*/
private static getRoutePath(...paths: string[]) {
return paths.filter(path => !!path).join(`/`);
}
private static canSendResponse(res: Express.Response) {
return !res.writableEnded;
}
}
type ApplicationOptions = {
app: Express.Application;
logRequests?: boolean;
};
/**
* Creates application core
* @param options.app - Express application used by application
* @param options.logRequests - log requests, enabled by default
*/
export function Application(options: ApplicationOptions): AppConfigurator {
return new AppConfigurator(options.app, options.logRequests);
}