@inpassor/node-server
TypeScript icon, indicating that this package has built-in type declarations

0.2.0 • Public • Published

Simple node.js HTTP / HTTPS 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.

Installation

npm install @inpassor/node-server --save

Usage

Server [class]

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 route shop/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.

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.

Component [class]

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');
  }
}

Component.app [property]

app: Server

Component.request [property]

request: Request

Component.response [property]

response: Response

Request [class]

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.

Response [class]

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. 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.

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. Automatically resolves by path.resolve function.
  • params: Params (default: undefined) - An object containing data to be used by a render function.

Helpers

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]

Examples

Stand-alone

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.

socket.io

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();
  },
});

Firebase Cloud functions

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

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

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.

Package Sidebar

Install

npm i @inpassor/node-server

Weekly Downloads

21

Version

0.2.0

License

MIT

Unpacked Size

67.5 kB

Total Files

88

Last publish

Collaborators

  • inpassor