Typescript friendly REST wrapper for express.js
Key features:
- IoC friendly, see invesify
- profiler support, see @ts-awesome/profiler
- validation support, see @ts-awesome/validate
- helper for PM2 integration
- health checks and managed resources ecosystem
- powerful route helper:
- ETag handling
- Accept handling
- model reader for input and output
import {httpGet, Route, useRoutes, useErrorHandler} from "@ts-awesome/rest";
import express from 'express';
@httpGet("/now")
class EchoRoute extends Route {
async handle() {
return this.json({
now: new Date()
});
}
}
const app = express();
app.use(useRoutes(
EchoRoute
));
// global error handler
app.use(useErrorHandler());
app.listen(3001, '0.0.0.0', () => {
console.info(`Server is listening http://0.0.0.0:3001`)
});
You can use all decorated routes and middlewares with useRestServer
.
Please make sure to import relevant files beforehand.
import {httpGet, Route, useRestServer, IoCSetup, useIoC} from "@ts-awesome/rest";
import express from 'express';
import {useContainerModules} from "./server";
@httpGet("/now")
class EchoRoute extends Route {
async handle() {
return this.json({
now: new Date()
});
}
}
// empty request scope binds
const rootIoCSetup: IoCSetup = () => [];
const requestIoCSetup: IoCSetup = () => [];
const app = express();
// setup root IoC container
app.use(useIoC(rootIoCSetup));
// bind all decorated routes and middlewares that are discoverable
app.use(useRestServer(requestIoCSetup));
app.listen(3001, '0.0.0.0', () => {
console.info(`Server is listening http://0.0.0.0:3001`)
});
Make things more convenient this library offect BaseApplication class
import {
BaseApplicationServer,
startDevServer,
IoCSetup,
useRestServer
} from "@ts-awesome/rest";
const rootIoCSetup: IoCSetup = () => [];
const requestIoCSetup: IoCSetup = () => [];
class DemoApplication extends BaseApplicationServer {
constructor() {
super(rootIoCSetup);
}
protected configure(app): void {
// bind all decorated routes and middlewares that are discoverable
app.use(useRestServer(requestIoCSetup));
}
}
// start local dev server, or use startPm2Server for prod
startDevServer(new DemoApplication());
Now you have 2 endpoints that provide insight on app health
Reports if app is ready to accept connections, has following response
{
"status":"UP",
"checks":[]
}
Reports if app is in a good shape, has following response
{
"status":"UP",
"checks":[]
}
Route handler typicly extends abstract Route
class and define handler
method
import {injectable} from "inversify";
import {
httpGet,
Route,
requestParam
} from "@ts-awesome/rest"
// http operation
@httpGet('/hello/:world')
@cacheControl('no-store')
class HelloWorldRoute extends Route {
async handle(
// handle param decorators
@queryParam('caps', Boolean, true) caps: boolean | null,
@requestParam('world', String) world: string,
): Promise<void> {
// reply with json
return this.json({hello: caps ? world.toUpperCase() : world});
}
}
- @httpAll(path: string, ...middlewares)
- @httpGet(path: string, ...middlewares)
- @httpPost(path: string, ...middlewares)
- @httpPut(path: string, ...middlewares)
- @httpPatch(path: string, ...middlewares)
- @httpDelete(path: string, ...middlewares)
- @httpHead(path: string, ...middlewares)
- @queryParam(name, Model) equivalent to model read
req.query[name]
- @bodyParam(name, Model) equivalent to model read
req.body[name]
- @requestParam(name, Model) equivalent to model read
req.params[name]
- @headerParam(name, Model) equivalent to model read
req.headers[name]
- @cookieParam(name, Model) equivalent to model read
req.cookies[name]
- @queryModel(Model) equivalent to model read
req.query
- @requestBody(Model) equivalent to model read
req.body
- @cacheControl(type) supports:
- immutable
- public
- private
- no-cache
- no-store
-
redirect - performs several kinds of redirects
-
empty - respond with empty response
-
json - respond with JSON response, also can be model read
-
jsonAsync - wait for promise and respond with json
-
text - respond with text response
-
stream - respond and re-stream data
-
ensureRequestMedia - check if client expects content type
-
getRequestMediaPriority - check how much client desires content type (0 is the most desired, null - non-desired)
-
getPreferredRequestMedia - return most desired content type of given list or null
-
ensureCacheControl - sends Cache-Control header if possible, invoked by all response helpers
-
setHeader - set response header value
-
isNewerContent - check
If-None-Match
andIf-Modified-Since
request headers -
isNewerModel - compute ETag and check with
isNewerContent
-
isNewerList - compute ETag and check with
isNewerContent
-
ensureModelETag - throw if client expects older ETag
-
setContentETag - set response ETag header
-
setModelETag - compute ETga and
setContentETag
-
setListETag - compute ETga and
setContentETag
-
validate - perform validation and throw
BadRequestError
if fails
Middlewares should implement IMiddleware
interface
Global middlewares are decorated with @middleware(priority)
.
Middlewares are invoked from highest to 0 (pre route)
and -1 to lowest (post route).
You can also list route specific middlewares in http operation decorates.
Note: such middleware has to be decorated with @injectable
.
@injectable
class SimpleMiddleware implements IMiddleware {
async handle(
req: IHttpRequest,
res: IHttpResponse,
// read header
@headerParam('Authorization', String, true) authorization
): Promise<void> {
// something should happen here
// throw any error to cancel route execution and go to error handler
}
}
Middleware can use handler params decorators to their benefit
Any error throw in middleware will cancel all subsequent middlewares and route, and go to Error handler middleware
A special middleware that implements IErrorMiddleware
.
Its handle
method excepts err, req and res.
By default server already uses such middleware. If for any reason you need to change it, please bind your implementation to IoC container
import {Container} from "inversify";
import {IErrorMiddleware, ErrorHandlerMiddlewareSymbol} from "@ts-awesome/rest";
const container: Container;
class CustomErrorHandler implements IErrorMiddleware {
async handle(err, req, res) {
// handle err, do not call res.end();
}
}
// some where IoC setup routine
container
.bind<IErrorMiddleware>(ErrorHandlerMiddlewareSymbol)
.to(CustomErrorHandler);
HTTP 403 Forbidden
HTTP 400 Bad Request
HTTP 404 Not Found
HTTP 401 Unauthorized
A generic error for non-standard situations. You can specify HTTP status code
Applications commonly may depends on external services like RDBMS or key-value storage.
Sometimes such services may require some setup and/or connection warm up.
For such cases we can use ManagedResource
. It provides:
-
get title()
- provides human-readable resource name -
async healthy()
- checks if resource is in a good shape -
async start()
- establish connection or other preparations -
async stop()
- close connections or other cleanup
Here is a sample PostgresPoolHealthExamination
import {Pool} from "pg";
import {IManagedResource} from "@ts-awesome/rest";
function sleep(ms = 1000): Promise<void> {
return new Promise(r => setTimeout(r, ms));
}
export class PostgresPoolHealthExamination implements IManagedResource {
readonly title = 'Postgres Pool';
public constructor(
private readonly pool: Pool
) {
}
async start() {
// eslint-disable-next-line no-constant-condition
let delay = 250;
while (!await this.healthy()) {
await sleep(delay);
delay = Math.min(3000, delay * 1.5);
}
}
async stop() {
try {
await this.pool.end();
} catch (e) {
// ignored
}
}
async healthy() {
let client: PoolClient | null = null;
try {
client = await this.pool.connect()
await client.query('SELECT 1');
return true;
} catch (e) {
return false;
} finally {
client?.release();
}
}
}
To use it, please bind it to your container as follows
import {Container} from "inversify";
import {Pool} from "pg";
import {ExternalResourceSymbol, IManagedResource} from "@ts-awesome/rest"
const container: Container;
const pool: Pool;
// somewhere in your IoC initialization routine
container
.bind<IManagedResource>(ExternalResourceSymbol)
.toConstantValue(new PostgresPoolHealthExamination(pool))
// name should be unique
.whenTargetNamed('postgres pool');
For cases when you only need health checks, please use IHealthChecker
and HealthExaminationSymbol
In 3.0.0 of this library,
IoCSetup
delegate should return readonly ContainerModule[]
array.
This change is done to make IoC setup cleaner and modular.
It's advised to break IoC configuration into parts, for example configuring ORM with PG driver may look like following
import {ContainerModule} from "inversify";
import {Pool} from "pg";
import {ExternalResourceSymbol, IManagedResource} from "@ts-awesome/rest"
import {SqlQueryDriverSymbol, SqlQueryBuilderSymbol} from "@ts-awesome/orm";
import {ISqlQuery, PgDriver, PgCompiler} from "@ts-awesome/orm-pg";
export function configureOrmPgModule(pool: Pool): ContainerModule {
return new ContainerModule(bind => {
bind<IManagedResource>(ExternalResourceSymbol)
.toConstantValue(new PostgresPoolHealthExamination(pool))
.whenTargetNamed('postgres pool')
bind<IQueryDriver<ISqlQuery>>(SqlQueryDriverSymbol)
.toDynamicValue(() => new PgDriver(pool))
bind<IBuildableQueryCompiler<ISqlQuery>>(SqlQueryBuilderSymbol)
.to(PgCompiler)
});
}
And ioc.config.ts
can be like following:
import {Config} from "@ts-awesome/config"
import {Pool, PoolConfig} from "pg";
const config = new Config()
export function setupRootModules(): readonly ContainerModule[] {
return [
configureLoggerModule(
config.get<ILoggerConfig>('logger'),
config.get<IReporterConfig>('reporter')
),
configureMailerModule(
config.get('mailer', MailerConfig),
),
configureOrmPgModule(
new Pool(config.get<PoolConfig>('db'))
),
// other root modules
];
}
export function setupRequestModules(): readonly ContainerModule[] {
return [
configureEntityServiceModuleFor(
// SomeDbModel
),
configureValidationModuleFor(
// SomeModelInput
),
];
}
Useful for local development env, eg for use with nodemon. Kills app on unhandled exceptions. App can be force-stopped with repeatable SIGTERM/SIGINT signals
Recommended to production use with PM2
May be freely distributed under the MIT license.
Copyright (c) 2022 Volodymyr Iatsyshyn and other contributors