Simple node.js HTTP / HTTPS server
npm install @inpassor/node-server


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.
``bash`
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.port: number
Depending on it a corresponding server instance will be created:
HttpServer or HttpsServer.
- (default: 80) - a port to be used by a server.options: HttpServerOptions | HttpsServerOptions
- (default: {}) - options to passpublicPath: string | string[]
to createServer function.
- (default: 'public') - a directory to be served byindex: string
staticHandler. Automatically resolves by path.resolve function.
- (default: 'index.html') - an index file name to be served bymimeTypes: { [extension: string]: string }
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.
- (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 headershandlers: Handler[]
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.
- (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/ matches the routeshop/audio/speakers-101
. In this case there will be two path parameters:
`typescript`
{
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:
- named parameter "id" expected to consist of numbers (0-9);
- 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 |?: .'shop/
For example:
You can also specify a type of a non-obligatory last named path parameter:
or
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.
#### Server.run [method]
run: () => HttpServer | HttpsServer
Runs an HttpServer | HttpsServer and returns its instance.
#### Server.handle [method]
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).
#### Server.use [method]
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:
`typescript
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');
}
}
`
#### Component.app [property]
app: Server
#### Component.request [property]
request: Request
#### Component.response [property]
response: Response
A derivative class of IncomingMessage. Has a few additional properties:
#### Request.app [property]
app: Server
#### Request.uri [property]
uri: string
Current route URI.
#### Request.params [property]
params: { [name: string]: string }
A named route parameters list.
#### Request.searchParams [property]
searchParams: URLSearchParams
#### Request.body [property]
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:
#### Response.app [property]
app: Server
#### Response.request [property]
request: Request
#### Response.send [method]
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.
#### Response.sendJSON [method]
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.
#### Response.sendError [method]
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;
}
#### Response.render [method]
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.extension: string
If a template is of Buffer type, it's converted to string.
- - A renderer will be determined by this extension.params: Params
For example, 'ejs' will be rendered by EJS renderer.
- (default: undefined) - An object containing
data to be used by a render function.
#### Response.renderFile [method]
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.params: Params
Automatically resolves by path.resolve function.
- (default: undefined) - An object containing
data to be used by a render function.
The library has a few helper functions:
#### formatBytes [function]
formatBytes: (bytes: number, decimals = 2) => string
#### getCodeFromError [function]
getCodeFromError: (error) => number
#### getMessageFromError [function]
getMessageFromError: (error) => string
#### resolvePath [function]
resolvePath: (...pathSegments) => string
#### httpStatusList [object]
httpStatusList: { [code: number]: string }
#### mimeTypes [object]
mimeTypes: { [extension: string]: string }
#### isHttpServerOptions [type guard]
isHttpServerOptions: (arg) => arg is ServerOptions
#### isHttpsServerOptions [type guard]
isHttpsServerOptions: (arg) => arg is ServerOptions
#### isServerConfig [type guard]
isServerConfig: (arg) => arg is ServerConfig
#### Logger [class]
`typescript
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',
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.
`typescript
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.
#### Common usage
`typescript
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',
});
`
#### Asynchronous Server config
`typescript
import { RuntimeOptions, HttpsFunction, runWith } from 'firebase-functions';
import { Server, ServerConfig } from '@inpassor/node-server';
const firebaseApplication = (
getConfig: ServerConfig | Promise
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
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.