@amphibian/server

A bare bones node.js server that makes setup and maintaining a breeze

Stats

stars 🌟issues ⚠️updated 🛠created 🐣size 🏋️‍♀️
Minified + gzip package size for @amphibian/server in KB

Readme

@amphibian/server

build status coverage report

A bare bones node.js server based on Koa that makes setup and maintaining a breeze.

import createServer from '@amphibian/server';

const server = createServer();

async function helloWorldHandler(context, next) {
    context.status = 200;
    context.body = {
        message: 'Hello World!',
        something: await operation()
    };
}

server.registerRouteHandler(helloWorldHandler, {
    method: 'get',
    path: '/hello-world'
});

Handlers

There are two methods for setting up handlers: registerHandler and registerRouteHandler. The first method, registerHandler, does not bind to any specific route and method, while the second, registerRouteHandler, does.

You will have to call await next() yourself if the handler should be skipped.

registerHandler

function normalHandler(context, next) {
    if (context.method.toLowerCase() === 'get') {
        context.body = {message: 'It works!'};
        return;
    }

    await next();
}

server.registerHandler(normalHandler);

registerRouteHandler

Takes three arguments: name, handler and options. name can be omitted. options takes two properties: method and path.

registerRouteHandler takes care of calling await next() for you.

function routeHandler(context) {
    context.body = {message: 'It works!'}
}

server.registerRouteHandler(routeHandler, {
    method: 'get',
    path: '/hello'
});

Middleware

Creating middleware is as easy as it gets. Just call next, the second handler argument, to pass the request onto the next handler in the list.

async function logTimeMiddleware(context, next) {
    console.time('request')
    await next();
    console.timeEnd('request');
}

server.registerMiddleware(logTimeMiddleware);

Note that registerMiddleware is exactly the same function as registerHandler – using it is syntactic sugar that logs “Registered middleware” instead of “Registered handler”.

Advanced routing

For reference, the provided route (whether it's a RegExp or a String) is placed in context.routePath. This might be useful for measuring the performance of each route.

async function logHandler(context) {
    console.log(context.routePath); // > /^\/message\/([^\/]+)$/
}

server.registerRouteHandler(logHandler, {
    method: 'get',
    path: /^\/message\/([^\/]+)$/
});

String with parameters

Provide path as a String containing parameters prefixed by :.

async function advancedStringHandler(context) {
    const {yourMessage} = context.args;
    context.status = 200;
    context.body = {message: `Your message is: ${yourMessage}`};
}

server.registerRouteHandler(advancedStringHandler, {
    method: 'get',
    path: '/message/:yourMessage'
});

Path parameter matches are placed as property of the context.args Object.

RegExp

To take advantage of RegExp routes using the built in router utility, simply send a regular expression instead of a String in the path key of the options object:

async function advancedRegExpHandler(context) {
    const [message] = context.args;
    context.status = 200;
    context.body = {message: `Your message is: ${message}`};
}

server.registerRouteHandler(advancedRegExpHandler, {
    method: 'get',
    path: /^\/message\/([^\/]+)$/
});

Any RegExp matches are placed as an Array in context.args.

Options object

An options object can be passed as an argument in createServer.

options.port (number)

Sets the port the server will listen on. Default: 4000

options.logging (boolean|'errors')

Enables or disables logging. Set to String errors to only log errors. Default: true

options.listen (boolean)

Useful when using @amphibian/server in conjunction with socket.io. Read more here. Default: true

Error handling

@amphibian/server will automatically handle errors by serving an error object to the body. The error object has two properties: code and message.

{
    "error": {
        "code": "unknown_error",
        "message": "Something went wrong"
    }
}

error.code will default to unknown_error if no code is set on the Error instance. The response.status will default to 500 unless the Error instance has a status property. You can safely throw errors from a handler:

async function throwingHandler(context) {
    const error = new Error('Something went wrong');
    error.status = 400;
    error.code = 'my_error_code';

    throw error;
}

server.registerRouteHandler(throwingHandler, {
    method: 'get',
    path: '/error'
});

Navigating to /error will yield the following response.body, with 400 as response.status:

{
    "error": {
        "code": "my_error_code",
        "message": "Something went wrong"
    }
}

Error boundaries

To do handle the errors before @amphibian/server spits them out, set up an error boundary:

async function errorBoundary(context, next) {
    try {
        await next();
    } catch (error) {
        if (error.code === 'fatal_error') {
            console.log(error.stack);
            throw new Error('camouflaged error');
        }

        throw error;
    }
}

server.registerMiddleware(errorBoundary);

Ensure the errorBoundary is the first middleware/handler you register. You won't be able to catch errors from those that come before it.

Prevent logging an error

To prevent @amphiban/server from logging an error, give the error a log property set to false.

const error = new Error('some_error');
error.log = false;
throw error;

Graceful shutdown

To avoid dropping active requests when receiving a SIGTERM signal, you should implement a SIGTERM signal handler on the node process. This will allow the server to finish active requests before shutting down.

The @amphibian/server server Object has a Function close that returns a Promise:

import createServer from '@amphibian/server';

const server = createServer();

process.on('SIGTERM', async () => {
    await server.close();
    process.exit();
});

New server requests that are received during shutdown will be refused as per net.server.close().

Usage with socket.io (as a koa callback)

import http from 'http';
import io from 'socket.io';
import createServer from '@amphibian/server';

const server = createServer({listen: false});
const httpServer = http.createServer(app.callback());
const socket = io(httpServer);

io.on('connection', /* ... */);
httpServer.listen(3000);

If you find any bugs or have a feature request, please open an issue on github!

The npm package download data comes from npm's download counts api and package details come from npms.io.