A simple HTTP / HTTPS server written in pure node.js without Express.
This library is designed to be minimalistic, quick and powerful at once. It includes two built-in middleware functions: staticHandler and routeHandler. They act one after another. First, staticHandler, then routeHandler. You can implement any number of middleware functions. They will be served first.
staticHandler serves static files under a public directory. It looks for directories/files by URI. If URI matches an existing directory, staticHandler looks for an index file within it. The library determines a few MIME types by a file extension. You can define additional MIME types in the Server config.
If URI does not match any public directory/file the Server runs routeHandler.
routeHandler serves routes and runs user-implemented Components. You can implement any REST method within Component. Routes are defined in the Server config.
npm install @inpassor/node-server --save
constructor(config?: ServerConfig)
Creates a Server instance with a given config. If config is omitted default options are used.
ServerConfig is an object with any set of these options:
-
protocol: 'http' | 'https'
(default: 'http') - a protocol to be used by a server. Depending on it a corresponding server instance will be created: HttpServer or HttpsServer. -
port: number
(default: 80) - a port to be used by a server. -
options: HttpServerOptions | HttpsServerOptions
(default: {}) - options to pass to createServer function. -
publicPath: string | string[]
(default: 'public') - a directory to be served by staticHandler. Automatically resolves by path.resolve function. -
index: string
(default: 'index.html') - an index file name to be served by staticHandler. If URI matches an existing directory under publicPath directory, staticHandler is looking for this file within this directory and renders it by a corresponding renderer. A renderer is determined by an index file extension. -
mimeTypes: { [extension: string]: string }
(default: {}) - additional MIME types by extension. For example:{ mp3: 'audio/mpeg', pdf: 'application/pdf', doc: 'application/msword', }
-
headers: { [name: string]: string }
(default: {}) - list of headers for all the server responses. For example:{ 'Access-Control-Allow-Methods': 'OPTIONS, GET, POST', 'Access-Control-Allow-Credentials': 'true', 'Access-Control-Allow-Headers': 'content-type, authorization', }
-
sameOrigin: boolean
(default: false) - when set to true adds headers Access-Control-Allow-Origin equal to request Origin header and Vary equal to 'Origin' to all the server responses. If a request does not contain Origin header, headers Access-Control-Allow-Origin and Vary are not added. -
handlers: Handler[]
(default: []) - additional middleware functions.Handler is a function
(request: Request, response: Response, next: () => void): void
.It accepts three arguments:
request: Request
- Request instance.response: Response
- Response instance.next: () => void
- A function that passes control to a next middleware function if is called inside a handler function.
You can also call Server.use method to add middleware after Server instance created.
-
routes: Route[]
(default: []) - routes to be served by routeHandler.Route is an object:
{ path: string; component: typeof Component; headers?: { [name: string]: string }; }
-
path: string
- A path pattern. You can specify named path parameters by enclosing parameters names in<...>
.For example: the path pattern
'shop/<category>/<item>'
matches the routeshop/audio/speakers-101
. In this case there will be two path parameters:{ category: 'audio', item: 'speakers-101', }
By default value of named path parameter can be any set of these symbols: a-zA-Z0-9-_.
You can specify path parameter type by adding to its name '|n' (for numbers) or '|l' (for letters).
Example:
<id|n>
- named parameter "id" expected to consist of numbers (0-9);<category|l>
- named parameter "category" expected to consist of latin letters (a-zA-Z).A last named path parameter can be non-obligatory. In this case, we need to "hide" the last slash inside the name and add |?:
</...|?>
. For example:'shop/<category></item|?>'
You can also specify a type of a non-obligatory last named path parameter:
</...|l?>
or</...|n?>
You can use "magic" path: '*', which matches any route. A Route with this path should be defined after all the routes.
-
component: typeof Component
- A derivative class of Component. -
headers: { [name: string]: string }
(default: {}) - Additional headers for this route.
-
-
renderers: { [extension: string]: Renderer }
(default: {}) - list of render functions by extension. For example:{ ejs: ejsRender, // don't forget to import { render as ejsRender } from 'ejs'; }
Renderer is a function
(template: string, params?: Params) => string
.It accepts one or two arguments:
template: string
- A template string.params: Params
(default: undefined) - An object containing data to be used by a render function.
Returns string - a result of render function to be sent to a client.
-
bodyParsers: { [mimeType: string]: BodyParser }
(default: { 'application/json': JSON.parse }) - list of parser functions by MIME type.BodyParser is a function
(body: string) => string
It accepts one argument:
body: string
- Request body.
Returns parsed body and stores it to Request.body.
-
maxBodySize: number
(default: 2097152 - 2Mb) - maximum size of Request body.
run: () => HttpServer | HttpsServer
Runs an HttpServer | HttpsServer and returns its instance.
handle: (request: Request, response: Response) => void
A Server handler. Called by Server.run method automatically. It can be used by an external server (for example, Firebase Cloud functions).
use: (handler: Handler) => void
Adds a middleware to a Server instance.
All the user-implemented Component classes for routeHandler should be derivative of Component class.
You can implement any REST method within a Component class. All you need to do is create a method of a class with a name coinciding with a request method name (in lower case). Create all method to serve all the request methods.
For example, this is DemoComponent implementing GET and POST methods:
class DemoComponent extends Component {
public get(): void {
this.response.renderFile([__dirname, 'demo-component.ejs'], {
title: 'Demo Component',
});
}
public post(): void {
this.response.send(200, 'This is the DemoComponent POST action');
}
}
app: Server
request: Request
response: Response
A derivative class of IncomingMessage. Has a few additional properties:
app: Server
uri: string
Current route URI.
params: { [name: string]: string }
A named route parameters list.
searchParams: URLSearchParams
body: string
A body of the request. Is parsed by BodyParser function if bodyParsers config option contain key equal to Content-Type request header.
A derivative class of ServerResponse. Has a few additional properties and methods:
app: Server
request: Request
send: (status: number, body?) => void
Sends a response to a client.
Accepts one or two arguments:
status: number
- HTTP status code.body: string
(default: undefined) - A body of response.
sendJSON: (data: string) => void
Sends a response to a client in JSON format.
Accepts one argument:
data: string
- A data to be sent in JSON format in a body of response.
sendError: (error) => void
Sends an error response to a client.
Accepts one argument:
error: unknown
- Error object. The library tries to get HTTP status code and error message automatically. Basically, error object should be as follows:{ code: number; message: string; }
render: (template: string | Buffer, extension: string, params?: Params) => void
Renders a template by a renderer, determined by extension, and responds to a client with a body, containing a result of a render function.
Accepts two or three arguments:
template: string | Buffer
- A template string or Buffer. If a template is of Buffer type, it's converted to string.extension: string
- A renderer will be determined by this extension. For example, 'ejs' will be rendered by EJS renderer.params: Params
(default: undefined) - An object containing data to be used by a render function.
renderFile: (pathSegments: string | string[], params?: Params) => void
Renders a file by a renderer, determined by a file extension, and responds to a client with a body, containing a result of a render function.
Accepts one or two arguments:
pathSegments: string | string[]
- A file name to be rendered. Automatically resolves by path.resolve function.params: Params
(default: undefined) - An object containing data to be used by a render function.
The library has a few helper functions:
formatBytes: (bytes: number, decimals = 2) => string
getCodeFromError: (error) => number
getMessageFromError: (error) => string
resolvePath: (...pathSegments) => string
httpStatusList: { [code: number]: string }
mimeTypes: { [extension: string]: string }
isHttpServerOptions: (arg) => arg is ServerOptions
isHttpsServerOptions: (arg) => arg is ServerOptions
isServerConfig: (arg) => arg is ServerConfig
import { Server, Component, ServerConfig } from '@inpassor/node-server';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { render as ejsRender } from 'ejs';
class ErrorComponent extends Component {
public all(): void {
this.response.sendError({
code: 405,
});
}
}
class DemoComponent extends Component {
public get(): void {
console.log(this.request.params);
this.response.renderFile([__dirname, 'demo-component.ejs'], {
title: 'Demo Component',
});
}
public post(): void {
console.log(this.request.params);
this.response.send(200, 'This is the DemoComponent POST action');
}
}
const config: ServerConfig = {
protocol: 'https', // 'http|https', default: 'http'
port: 8080, // default: 80
options: {
// ServerOptions for HTTP or HTTPS node.js function createServer, default: {}
key: readFileSync(resolve(__dirname, 'certificate.key.pem')),
cert: readFileSync(resolve(__dirname, 'certificate.crt.pem')),
ca: readFileSync(resolve(__dirname, 'certificate.fullchain.pem')),
},
publicPath: 'public', // path to public files, default: 'public'
index: 'index.html', // index file name, default: 'index.html'
mimeTypes: {
// additional MIME types
mp3: 'audio/mpeg',
pdf: 'application/pdf',
doc: 'application/msword',
},
headers: {
// list of headers for all the server responses, default: {}
'Access-Control-Allow-Methods': 'OPTIONS, GET, POST',
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Allow-Headers': 'content-type, authorization',
},
sameOrigin: true, // when set to true adds headers 'Access-Control-Allow-Origin' equal to
// request Origin header and 'Vary' equal to 'Origin' to all the server responses
handlers: [], // additional middleware functions
// (you can also call Server.use method to add middleware after Server instance created)
routes: [
// routes to be served by routeHandler
{
path: 'demo</arg|?>',
component: DemoComponent,
},
{
path: '*',
component: ErrorComponent,
},
],
renderers: {
// list of render functions
ejs: ejsRender,
},
};
const server = new Server(config);
// Add middleware
server.use((request, response, next) => {
// TODO: some middleware work
// call next function to pass work to next middleware
// next();
// or send a response to a client, otherwise, the server will hang till timeout
// use Response.send method in order to send all the needed headers defined in the config
response.send(200, 'Some content');
});
server.run();
We had created a Server instance with ejs renderer and two components: DemoComponent, having GET and POST methods, and ErrorComponent, serving all the routes (which did not match any previous route) and all the request methods.
The route /demo[/arg] will be served by DemoComponent.
All the other routes will be served first under publicPath directory, then ErrorComponent will act.
import { Server, ServerConfig } from '@inpassor/node-server';
import * as socketIO from 'socket.io';
const config: ServerConfig = {}; // define your own ServerConfig here
const server = new Server(config);
const serverInstance = server.run(); // instance of HTTP or HTTPS node.js Server
const io = socketIO(serverInstance, {
handlePreflightRequest: (request, response) => {
response.writeHead(204, {
'Access-Control-Allow-Methods': 'OPTIONS, GET',
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Allow-Headers': 'content-type, authorization',
'Access-Control-Allow-Origin': request.headers.origin,
Vary: 'Origin',
});
response.end();
},
});
There is no need for HTTP or HTTPS node.js server instance since Firebase Cloud functions create its own server. We just need to pass Server.handle method to Firebase.
import { RuntimeOptions, HttpsFunction, runWith } from 'firebase-functions';
import { Server, ServerConfig } from '@inpassor/node-server';
const firebaseApplication = (
config: ServerConfig,
runtimeOptions?: RuntimeOptions
): HttpsFunction => {
const server = new Server(config);
return runWith(runtimeOptions).https.onRequest(server.handle.bind(server));
};
const config: ServerConfig = {}; // define your own ServerConfig here
export const firebaseFunction = firebaseApplication(config, {
timeoutSeconds: 10,
memory: '128MB',
});
import { RuntimeOptions, HttpsFunction, runWith } from 'firebase-functions';
import { Server, ServerConfig } from '@inpassor/node-server';
const firebaseApplication = (
getConfig: ServerConfig | Promise<ServerConfig>,
runtimeOptions?: RuntimeOptions
): HttpsFunction => {
return runWith(runtimeOptions).https.onRequest(async (request, response) => {
await new Promise((resolve, reject) => {
Promise.resolve(getConfig).then(
(config): void => {
const server = new Server(config);
resolve(server.handle.call(server, request, response));
},
(error) => reject(error)
);
});
});
};
// Some asynchronous get config function
const getConfig = (): Promise<ServerConfig> => {
const config: ServerConfig = {}; // define your own ServerConfig here
return Promise.resolve(config);
};
export const firebaseFunction = firebaseApplication(getConfig(), {
timeoutSeconds: 10,
memory: '128MB',
});
You can also use the library @inpassor/firebase-application which wraps node-server.