Experimental Node.js HTTP framework using RxJS, built with TypeScript and optimized for serverless deployments
npm install serverx-ts ![Jest Coverage]() ![npm]() ![node]() 

Experimental Node.js HTTP framework using RxJS, built with TypeScript and optimized for serverless deployments. Heavily inspired by Marble.js and NestJS.
- Rationale
- Design Objectives
- Design Non-Objectives
- Some Bookmarks for Future Work
- Key Concepts
- Sample Application
- Primer
- Serverless Support
- AWS Lambda Considerations
- Google Cloud Functions Considerations
- Messages
- Handlers
- Middleware
- Immediate Response
- Built-in Middleware
- Available Middleware
- Services
- Routing
- Inheritance
- Path Parameters
- Redirect
- Route Data
- File Server
- OpenAPI
- Informational Annotations
- Metadata Annotations
> See ServeRX-serverless for a sample app operating in a serverless environment.
> See ServeRX-angular to see ServeRX-ts put to the test of deploying and hosting any Angular app without change and with no dependencies..
> ServeRX-ts is an experimental project only. It doesn't advocate replacing any other framework and certainly not those from which it has drawn extensively.
- _Declarative routes_ like Angular
- _Functional reactive programming_ using RxJS like Marble.js
- _Dependency injection_ like Angular and NestJS
- _Serverless support_ out-of-the-box for AWS Lambda with functionality similar to AWS Serverless Express but without the overhead
- _Serverless support_ out-of-the-box for Google Cloud HTTP Functions
- _Low cold-start latency_ as needed in serverless deployments, where in theory every request can trigger a cold start
- _Optimized for microservices_ in particular those that send application/json responses and typically deployed in serverless environments
- _OpenAPI support_ out-of-the-box to support the automated discovery and activation of the microservices for which ServeRX-ts is intended via the standard OpenAPI specification
- _Full type safety_ by using TypeScript exclusively
- _Maximal test coverage_ using Jest
- _Deployment of static resources_ which can be commoditized via, for example, a CDN. However, ServeRX-ts supplies a simple but effective FileServer handler that has just enough capability to deploy (say) an Angular app.
- _FRP religion_ ServeRX-ts believes in using functions where appropriate and classes and class inheritance where they are appropriate
- _Emulator for Express middleware_ (but that's hard and definitely back-burner!)
Like Marble.js, linear request/response logic is not used to process HTTP traffic. Instead, application code operates on an observable stream. ServeRX-ts does not provide any abstractions for server creation. Either standard Node.js APIs are used or appropriate serverless functions.
ServeRX-ts _does_ however abstract requests and responses (whatever their source or destination) and bundles them into a stream of messages.
A Handler is application code that observes this stream, mapping requests into responses.
Similarly, middleware is code that maps requests into new requests and/or responses into new responses. For example, CORS middleware takes note of request headers and injects appropriate response headers.
Services can be injected into both handlers and middleware. ServeRX-ts uses the injection-js dependency injection library, which itself provides the same capabilities as in Angular 4. In Angular, services are often used to provide common state, which makes less sense server-side. However in ServeRX-ts, services are a good means of isolating common functionality into testable, extensible and mockable units.
DI is also often used in ServeRX-ts to inject configuration parameters into handlers, middleware and services.
Routes are the backbone of a ServeRX-ts application. A route binds an HTTP method and path to a handler, middleware and services. Routes can be nested arbitrarily in parent/child relationships, just like Angular. Middleware and services (and other route attributes) are inherited from a parent.
Routes can be annotated with OpenAPI metadata to enable the automated deployment of an OpenAPI YAML file that completely describes the API that the ServeRX-ts application implements.
``ts
import 'reflect-metadata';
import { AWSLambdaApp } from 'serverx-ts';
import { Compressor } from 'serverx-ts';
import { CORS } from 'serverx-ts';
import { GCFApp } from 'serverx-ts';
import { Handler } from 'serverx-ts';
import { HttpApp } from 'serverx-ts';
import { Injectable } from 'injection-js';
import { Message } from 'serverx-ts';
import { Observable } from 'rxjs';
import { RequestLogger } from 'serverx-ts';
import { Route } from 'serverx-ts';
import { createServer } from 'http';
import { tap } from 'rxjs/operators';
@Injectable()
class HelloWorld extends Handler {
handle(message$: Observable
return message$.pipe(
tap(({ response }) => {
response.body = 'Hello, world!';
})
);
}
}
const routes: Route[] = [
{
path: '',
methods: ['GET'],
middlewares: [RequestLogger, Compressor, CORS],
children: [
{
path: '/hello',
handler: HelloWorld
},
{
// NOTE: default handler sends 200
// for example: useful in load balancers
path: '/isalive'
},
{
path: '/not-here',
redirectTo: 'http://over-there.com'
}
]
}
];
// local HTTP server
const httpApp = new HttpApp(routes);
createServer(httpApp.listen()).listen(4200);
// AWS Lambda function
const lambdaApp = new AWSLambdaApp(routes);
export function handler(event, context) {
return lambdaApp.handle(event, context);
}
// Google Cloud HTTP Function
const gcfApp = new GCFApp(routes);
export function handler(req, res) {
gcfApp.handle(req, res);
}
`
> Be sure to include the following options in tsconfig.json when you build ServeRX-ts applications:
`json`
{
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true
}
}
> See ServeRX-serverless for a sample app operating in a serverless environment.
> There's just enough information here to understand the principles behind ServeRX-ts. Much detail can be learned from interfaces.ts and from the Jest test cases, which are in-line with the body of the code.
AWS Serverless Express connects AWS Lambda to an Express application by creating a proxy server and routing lambda calls through it so that they appear to Express code as regular HTTP requests and responses. That's a lot of overhead for a cold start, bearing in mind that in theory every serverless request could require a cold start.
Google Cloud Functions take a different approach and fabricate Express request and response objects.
ServeRX-ts attempts to minimize overhead by injecting serverless calls right into its application code. This approach led a number of design decisions, notably messages, discussed next.
ServeRX-ts recommends using the excellent serverless framework to deploy to serverless environments.
#### AWS Lambda Considerations
> TODO: discuss how to control binary types and recommended serverless.yml.
#### Google Cloud Functions Considerations
> TODO: ??? and recommended serverless.yml.
ServeRX-ts creates messages from inbound requests (either HTTP or serverless) and represents the request and response as simple inner objects.
| message.context | message.request | message.response |info: InfoObject
| ------------------ | ------------------------ | -------------------- |
| | body: any | body: any |router: Router
| | headers: any | headers: any |httpVersion: string
| | | statusCode: number |method: string
| | |params: any
| | |path: string
| | |query: URLSearchParams
| | |remoteAddr: string
| | |route: Route
| | |timestamp: number
| | |
messages are strictly mutable, meaning that application code cannot create new ones. Similarly, inner request and response should be mutated. A common mutation, for example, is to add or remove request or response headers.
The job of a ServeRX-ts handler is to populate message.response, perhaps by analyzing the data in message.request.
If response.headers['Content-Type'] is not set, then ServeRX-ts sets it to application/json. If response.statusCode is not set, then ServeRX-ts sets it to 200.
All handlers must implement a handle method to process a stream of messages. Typically:
`ts`
@Injectable() class MyHandler extends Handler {
handle(message$: Observable
return message$.pipe( ... );
}
}
The job of ServeRX-ts middleware is to prepare and/or post-process streams of messages. In a framework like Express, programmers can control when middleware is executed by the appropriate placement of app.use() calls. Because routing in ServeRX-ts is declarative, it uses a different approach.
All ServeRX-ts middleware must implement either a prehandle or a posthandle method, or in special circumstances, both. All prehandle methods are executed before a handler gains control and all posthandle methods afterwards. Otherwise the shape of middleware is very similar to that of a handler:
`ts`
@Injectable() class MyMiddleware extends Middleware {
prehandle(message$: Observable
return message$.pipe( ... );
}
}
A third entrypoint exists: the postcatch method is invoked after all posthandle methods and even after an error has been thrown. The built-in RequestLogger middleware uses this entrypoint to make sure that _all_ requests are logged, even those that end in a failure.
> The postcatch method cannot itself cause or throw an error.
#### Immediate Response
middleware code can trigger an immediate response, bypassing downstream middleware and any handler by simply throwing an error. A good example might be authentication middleware that rejects a request by throwing a 401 error.
> A handler can do this too, but errors are more commonly thrown by middleware.
`ts`
import { Exception } from 'serverx-ts';
// more imports
@Injectable()
class Authenticator extends Middleware {
prehandle(message$: Observable
return message$.pipe(
// more pipeline functions
mergeMap((message: Message): Observable
return iif(
() => !isAuthenticated,
// NOTE: the format of an Exception is the same as a Response
throwError(new Exception({ statusCode: 401 })),
of(message)
);
})
);
}
}
#### Built-in Middleware
- The Normalizer middleware is automatically provided for all routes and is guaranteed to run after all other posthandlers. It makes sure that response.headers['Content-Length'], response.headers['Content-Type'] and response.statusCode are set correctly.
- The BodyParser middleware is automatically provided, except in serverless environments, where body parsing is automatically performed.
#### Available Middleware
- The Compressor middleware performs request.body gzip or deflate compression, if accepted by the client. See the compressor tests for an illustration of how it is used and configured.
- The CORS middleware is a wrapper around the robust Express CORS middleware. See the CORS tests for an illustration of how it is used and configured.
- The RequestLogger middleware is a gross simplification of the Express Morgan middleware. See the request logger tests for an illustration of how it is used and configured.
- The Timer middleware injects timing information into response.header. See the timer tests for an illustration of how it is used.
> TODO: discuss default LogProvider and possible Loggly log provider.
ServeRX's routes follow the pattern set by Angular: they are declarative and hierarchical. For example, the following defines two routes, GET /foo/bar and PUT /foo/baz:
`ts`
const routes: Route[] = {
{
path: '/foo',
children: [
{
methods: ['GET'],
path: '/bar',
Handler: FooBar
},
{
methods: ['PUT'],
path: '/baz',
Handler: FooBaz
}
]
}
};
> Notice how path components are inherited from parent to child. Parent/child relationships can be arbitrarily deep.
#### Inheritance
Paths are not the only route attribute that is inherited; methods, middleware and services are too. Consider this example:
`ts`
const routes: Route[] = [
{
path: '',
methods: ['GET'],
middlewares: [RequestLogger, CORS],
services: [{ provide: REQUEST_LOGGER_OPTS, useValue: { colorize: true } }]
children: [
{
path: '/bar',
Handler: FooBar
},
{
path: '/baz',
services: [{ provide: LogProvider, useClass: MyLogProvider }]
Handler: FooBaz
}
]
}
];
> Notice how an empty path component propagates inheritance but doesn't affect the computed path.
Routes for GET /bar and GET /baz are defined. Both share the RequestLogger and CORS middleware, the former nominally configured to colorize its output. However, GET /baz uses its own custom LogProvider.
ServeRX-ts leverages inheritance to inject its standard middleware and services without special code. The Router takes the supplied routes and wraps them like this:
`ts`
{
path: '',
middlewares: [BodyParser / HTTP only /, Normalizer],
services: [LogProvider],
children: [ / supplied routes / ]
}
#### Path Parameters
Path parameters are coded using OpenAPI notation:
`ts`
{
methods: ['GET'],
path: '/foo/{this}',
handler: Foo
},
{
methods: ['GET'],
path: '/foo/{this}/{that}',
handler: Foo
}
> Notice how optional parameters are coded by routing variants to the same handler.
Path parameters are available to handlers in message.request.params.
#### Redirect
A redirect can be coded directly into a route:
`ts`
{
methods: ['GET', 'PUT', 'POST'],
path: '/not-here',
redirectTo: 'http://over-there.com',
redirectAs: 307
}
> If redirectAs is not coded, 301 is assumed.
#### Route Data
An arbitrary data object can be attached to a route:
`ts`
{
data: { db: process.env['DB'] },
methods: ['GET'],
path: '/foo/bar/baz',
handler: FooBarBaz
}
> Route data can be accessed by both middleware and a handler via message.request.route.data.
ServeRX-ts supplies a simple but effective FileServer handler that has just enough capability to deploy (say) an Angular app. It can be used in any route, for example:
`ts`
const routes: Route[] = [
{
path: '',
children: [
{
methods: ['GET'],
path: '/public',
handler: FileServer,
provide: [
{ provide: FILE_SERVER_OPTS, useValue: { maxAge: 999, root: '/tmp' } }
]
}
// other routes
]
}
];
By default, it serves files starting from the user's home directory, although that can be customized as shown above. So in that example, GET /public/x/y/z.js would attempt to serve /tmp/x/y/z.js.
ServeRX-ts forces must-revalidate caching and sets max-age as customized or one year by default. The file's modification timestamp is used as an Etag to control caching via If-None-Match.
ServeRX-ts supplies an OpenAPI handler that can be used in any route, although by convention:
`ts
const routes: Route[] = [
{
path: '',
children: [
{
path: 'openapi.yml',
handler: OpenAPI
}
// other routes
]
}
];
const app = new HttpApp(routes, { title: 'http-server', version: '1.0' });
`
The OpenAPI handler creates a YAML response that describes the entire ServeRX-ts application.
> Notice how an InfoObject can be passed to HttpApp, AWSLambdaApp and so on to fulfill the OpenAPI specification. The excellent OpenApi3-TS package is a ServeRX-ts dependency and its model definitions can be imported for type-safety.
#### Informational Annotations
Routes can be annotated with summary and description information:
`ts`
const routes: Route[] = [
{
path: '',
methods: ['GET'],
summary: 'A family of blah blah endpoints',
children: [
{
description: 'Get some bar-type data',
path: '/bar',
Handler: FooBar
},
{
description: 'Get some baz-type data',
path: '/baz',
Handler: FooBaz
}
]
}
];
> Both summary and description are inherited.
> Because ServeRX-ts is biased toward microservices, ServeRX-ts does not currently support the many other informational annotations that the full OpenAPI specification does.
#### Metadata Annotations
Routes can also be annotated with request and responses metadata. The idea is to provide OpenAPI with decorated classes that describe the format of headers, parameters and request/response body. These classes are the same classes that would be used in middleware and handlers for type-safety. The request and responses annotations are inherited.
Consider the following classes:
`ts
class CommonHeader {
@Attr({ required: true }) x: string;
@Attr() y: boolean;
@Attr() z: number;
}
class FooBodyInner {
@Attr() a: number;
@Attr() b: string;
@Attr() c: boolean;
}
class FooBody {
@Attr() p: string;
@Attr() q: boolean;
@Attr() r: number;
// NOTE: _class is only necessary because TypeScript's design:type tells us
// that a field is an array, but not of what type -- when it can we'll deprecate
@Attr({ _class: FooBodyInner }) t[]: FooBodyInner;
}
class FooPath {
@Attr() k: boolean;
}
class FooQuery {
@Attr({ required: true }) k: number;
}
`
They could be used in the following routes:
`ts`
const routes: Route[] = [
{
path: '',
request: {
header: CommonHeader
},
children: [
{
methods: ['GET'],
path: '/foo',
request: {
path: FooPath,
query: FooQuery
}
},
{
methods: ['PUT'],
path: '/foo',
request: {
body: {
'application/x-www-form-urlencoded': FooBody,
'application/json': FooBody
}
}
},
{
methods: ['POST'],
path: '/bar',
responses: {
'200': {
'application/json': BarBody
}
}
}
]
}
];
> Notice how request and responses are inherited cumulatively.
When ServeRX-ts wraps supplied routes, it automatically adds metadata about the 500 response it handles itself, as if this were coded:
`ts``
{
path: '',
middlewares: [BodyParser / HTTP only /, Normalizer],
services: [LogProvider],
responses: {
'500': {
'application/json': Response500
}
},
children: [ / supplied routes / ]
}