2.1.5 • Published 4 years ago

node-backend-framework v2.1.5

Weekly downloads
3
License
ISC
Repository
gitlab
Last release
4 years ago

Node Backend framework

A Node backend framework extending Express written in TypeScript. It introduces the Controller principle known from other various frameworks. It has built-in authorization handling, easy CORS configuration via environment variables and out of the box Sentry support.

Using TypeScript with this framework is highly recommended! All shown examples are in TypeScript. Also, Yarn is used in the examples, but NPM should work fine too.

Initializing a backend project

Initialize a Node project:

yarn init

Install the framework:

yarn add node-backend-framework

Initialize a backend project:

npx be-init # or just `be-init` when globally installed

NOTE: Using Visual Studio Code as IDE is highly recommended!

App class

The App class is the default export of the application and will initialize the controllers, middleware and error handler. It will also check for a .env file and inject its content into process.env when found.

Use it like this:

import App from 'node-backend-framework'; // make sure to import first so .env is injected from the start
import express from 'express';
import HelloWorldController from './controllers/HelloWorldController';

const app = new App(
    [new HelloWorldController()],
    [express.json()] // optional, Express middleware to be used
);

app.start(`Server started at port ${app.port}`); // start with custom start up message

export default app;

To specify a middleware path:

const app = new App(
    [new HelloWorldController()],
    [{ middleware: express.json(), mPath: '/' }]
);

Controllers

A controller will take care of routing and handles requests. The routes can be grouped by the programmer this way. It also contains a default authorization handler which will check for a Bearer JWT in the Authorization header. It will try to verify the JWT with the secret found in the JWT_SECRET environment variable.

A controller can be set up like this:

import {
    Controller,
    Method,
    Route,
    XRequest,
    XResponse
} from 'node-backend-framework';

/**
 * Controller regarding Hello world routes.
 */
export default class HelloWorldController extends Controller {
    readonly path = '/hello'; // obligated override, set the base path for this controller
    readonly routes: readonly Route[] = [
        {
            path: '/world', // the subpath, will result in => /hello/world
            requestHandler: this.helloWorld, // the function which will handle the request
            method: Method.GET // HTTP method
        }
    ]; // obligated override, set the controller's routes

    /**
     * Return JSON with Hello world message.
     * @param _req Express request object.
     * @param res Express response object.
     * @returns Express JSON response with Hello world message.
     */
    private async helloWorld(
        _req: XRequest, // prepended with _ since it remains unused (linter)
        res: XResponse
    ): Promise<XResponse> {
        return res.json({ message: 'Hello world' });
    }
}

Constructor parameters:

* = optional

PropertyTypeDescriptionDefault
parent*Controller?The parent controller. The path of the child will be appended to its parent which results in the full path.Undefined

Overridable properties:

PropertyTypeDescriptionDefault
pathstringThe base path of the controller's routes.N/A
routesRoute[]The routes of the controller.N/A
auth*boolean?Enable authorization for all the controller's routes except for the ones which explicitly set auth to false.false
serverErrorResponse*(XResponse) => XResponseSet the default 500 error response for the controllerresponse.status(500).json({ message: 'Something went wrong, try again later.' });
authHandler*AuthHandlerSets a custom authorization handler function for this controller when auth for this or a Route inside is true.Checks Authorization header for Bearer JWT which looks like: Bearer ey.... When authorised, sets decodedJwt on the XRequest and returns true, else it just returns false.
roles*Role[]?Sets roles which may access this controller. Can be overridden for a specific route by setting it on that route.undefined
roleKey*stringThe key of the role in the decoded JWT. Can be overriden for a specific route by setting it on that route.'role'

Routes

A route is represented as an instance of the Route interface. A route is part of a Controller instance, read Controller to see it in action.

Route Properties:

* = optional

PropertyTypeDescriptionDefault
requestHandlerRequestHandlerThe function which will handle the request.N/A
path*string?The path to follow. It is appended to its controller's path in the request URL (see Controller example).''
auth*boolean?When set to true, the authHandler is called before the request handler.Its controller's auth property value.
method*Method?The HTTP method to use, when it's undefined it uses all of them.undefined
authHandler*AuthHandler?Sets a custom authorization handler function for the specific route when auth is true. Overrides its controller's AuthHandler function for the route.Its controller's authHandler.
roles*Role[]?Sets roles which may access this route. Overrides the controller's roles for this route.Controller's roles.
roleKey*stringThe key of the role in the decoded JWT. Overrides the controller roleKey for this route.Controller's roleKey.

Method

A method enum has been created so invalid HTTP methods can't be accidentally passed to routes.

export enum Method {
    GET = 'GET',
    HEAD = 'HEAD',
    POST = 'POST',
    PUT = 'PUT',
    DELETE = 'DELETE',
    CONNECT = 'CONNECT',
    OPTIONS = 'OPTIONS',
    TRACE = 'TRACE'
}

Role

A role is a type alias for a string, number or the following interface:

{ value: number | string; allowHigher?: boolean; allowLower?: boolean }

The value will be compared to the allowed roles, after which will be determined if the user is authorised or not. When setting allowHigher it allows values higher than the allowed role value. When setting allowLower it allows values lower than the allowed role value.

RequestHandler

RequestHandler is a type alias for (XRequest, XResponse) => Promise<XResponse>.

A RequestHandler looks like this:

    /**
     * Return JSON with Hello world message.
     * @param _req Express request object.
     * @param res Express response object.
     * @returns Express JSON response with Hello world message.
     */
    private async helloWorld(
        _req: XRequest, // prepended with _ since it isn't used (linter)
        res: XResponse
    ): Promise<XResponse> {
        return res.json({ message: 'Hello world' });
    }

AuthHandler

AuthHandler is a type alias for (XRequest) => Promise<boolean>.

The default AuthHandler is implemented like this:

/**
* The default AuthHandler to run when `auth` of the controller or route is set to true.
* This method may be overridden by the child controllers
* to allow different auth handling between controllers.
* By default, it checks for a Bearer JWT in the Authorization header and tries to verify it.
* If successful, the decodedJwt will be injected into the Request object and the method will return true, else false.
* @param request Express request object.
* @param roleKey The key of the decoded JWT which has a role value.
* @param roles The roles to allow.
* @returns true if authorised, else false.
*/
protected async authHandler(
        request: XRequest,
        roleKey: string,
        roles?: readonly Role[]
    ): Promise<boolean> {
        if (!process.env.JWT_SECRET) {
            throw Error('No JWT_SECRET environment variable set.');
        }
        const authHeader = request.headers.authorization;
        if (authHeader === undefined) return false;
        if (!authHeader.startsWith('Bearer ')) return false;
        const token = authHeader.replace(/^(Bearer )/, '');
        try {
            const decoded: any = jwt.verify(token, process.env.JWT_SECRET);
            if (decoded instanceof String) return false;
            if (
                roles &&
                (decoded[roleKey] === undefined ||
                    !roles.find(role => {
                        if (role instanceof Object) {
                        if (role.allowHigher) {
                            return decoded[roleKey] >= role.value;
                        }
                        if (role.allowLower) {
                            return decoded[roleKey] <= role.value;
                        }
                    }
                    return decoded[roleKey] === role;
                }))
        )
            return false;
        request.decodedJwt = decoded;
        return true;
    } catch {
        // ignored
    }
    return false;
}

Requests & Responses

A request and response are wrapped into a XRequest and XResponse object respectively.

XRequest extends express.Request with the decodedJwt property.

As of now XResponse is only an alias of express.Response.

Read this for more information.

CORS

CORS is disabled by default and can be enabled using the CORS_WHITELIST environment variable.

Allow all domains CORS_WHITELIST=*.

Allow certain domains CORS_WHITELIST=["https://localhost:8080", "http://localhost:8081"].

NOTE: When using an array, it has to be a JSON array string (so double quotes only!).

Throw an exception when CORS is not allowed: CORS_ERROR=CORS disallowed.

Tasks & CronJobs

Tasks are represented as async functions which return nothing (void).

Tasks can be executed with the command line using the be-task command. Without globally installing this framework, it can be done using NPX. By default, the CLI will look into the dist/tasks folder. This can be overridden by setting the TASK_DIR environment variable. The dist/tasks folder contains the JavaScript files which have been compiled from the tasks source. The files will be auto compiled.

Task CLI options and examples:

OptionDescription
-A, --allRun all the tasks in the task directory.
-T, --task \<file name>Run a single task by file name.
-d, --dir \<Task directory>Set the task directory, default is determined by environment variable TASK_DIR, when it's undefined: ./dist/tasks.
-V, --versionPrints version number.
-h, --helpPrints option information.

Running all tasks:

yarn task -A # or --all

Running a single task:

yarn -T calculate  # (.js), or --task

Running tasks from other directory:

yarn task -A -d ./src/crons # or --dir

To run the tasks without auto-compiling (it has to be compiled earlier), replace yarn task with npx be-task

Cron jobs are represented by instances of the CronJob class. It takes a crontab schedule string or Date object to schedule, a task to run on schedule and optionally a onComplete function which is executed when the cron job is stopped (stop() is called). It must then be passed to the App instance. The timezone can be set using the TIMEZONE environment variable.

NOTE: Don't use this in production, but run your tasks from the CLI using a scheduler since this blocks the I/O stream.

Example:

import App, { CronJob } from 'node-backend-framework';
import express from 'express';
import HelloWorldController from './controllers/HelloWorldController';
import calculateTask from './tasks/calculate';

const app = new App(
    [new HelloWorldController()],
    [express.json()],
    [new CronJob('0 12 * * *', calculateTask)] // everyday at 12:00
);

app.start(`Server started at port ${app.port}`); // start with custom start up message, crons are executed by schedule from here

export default app;

Sentry

This framework has Sentry error monitoring support built-in. Sentry will automatically be enabled when the environment variable SENTRY_URL is set. The value will then be used as the dsn property of the initialization options. To use a advanced config, pass the configs to the App constructor:

import App, { MiddlewareError } from 'node-backend-framework';
import express from 'express';
import { CaptureConsole } from '@sentry/integrations';

import HelloWorldController from './controllers/HelloWorldController';

const app = new App(
    [new HelloWorldController()],
    [express.json()],
    undefined,
    {
        dsn: 'https://example@3mp1e.ingest.sentry.io/0000000', // will use this value instead of the SENTRY_URL environment variable
        integrations: [
            new CaptureConsole({
                levels: ['error']
            })
        ]
    }, // Sentry init config
    { ip: '127.0.0.1' }, // Sentry request handler options
    { shouldHandleError: (error: MiddlewareError) => error.status > 499 } // Sentry error handler options, handle error when status code is 500 or higher
);

app.start(`Server started at port ${app.port}`); // start with custom start up message, crons are executed by schedule from here

export default app;

NOTE: Integrations are required to be installed separately:

yarn add @sentry/integrations

For more documentation about configuring Sentry, read https://docs.sentry.io/platforms/node/express/.

HttpException

Can be thrown to return an error response to the client.

export default class HttpException extends Error {
    readonly status: number; // HTTP status code
    readonly message: string; // message in the JSON response body when status < 500
    constructor(status: number, message: string) {
        super(message);
        this.status = status;
        this.message = message;
    }
}

Environment variables

KeyPossible values
PORTThe port to run the application on. Default: 8080. Can be overwritten by the port parameter of the App constructor.
JWT_SECRETA string representing the JWT secret.
CORS_WHITELIST* or something like ["https://localhost:8080", "http://localhost:8081"]
CORS_ERRORWhen set, will throw an exception with this value as the message when CORS is not allowed.
TASK_DIRThe directory in which the tasks are located. When undefined: ./dist/tasks.
TIMEZONEThe timezone to use for the cron jobs.*
SENTRY_URLWhen set, enables Sentry using this value as the Sentry DSN. Can be overriden with the Sentry initialization options of the App constructor.

* = Look here for supported timezones.

Dependencies

This framework uses the following dependencies.

PackageDescription
ExpressTo make this entire framework's routing.
Commander, Figlet, Chalk, InquirerFor creating the task and init CLIs.
CORSTo enable and configure CORS.
CronFor scheduling and executing cron jobs.
DotenvTo inject the .env file into process.env.
JSON Web TokenTo verify JWTs in the default AuthHandler.
Official Sentry SDK For NodeJSTo support Sentry error monitoring out of the box.

Tests

Tests are written with Jest and Supertest and can be found in the __tests__ folder.

Tests can be run with

yarn test

Contribution

Contribution is welcome via GitLab :), please respect the TypeScript, ESLint and Prettier configurations and use Yarn instead of NPM.

More information

Read the Express documentation for more information about how this framework works and how to extend it.

2.1.5

4 years ago

2.1.4

4 years ago

2.1.3

4 years ago

2.1.2

4 years ago

2.1.1

4 years ago

2.1.0

4 years ago

2.0.14

4 years ago

2.0.13

4 years ago

2.0.12

4 years ago

2.0.11

4 years ago

2.0.10

4 years ago

2.0.9

4 years ago

2.0.8

4 years ago

2.0.7

4 years ago

2.0.6

4 years ago

2.0.5

4 years ago

2.0.4

4 years ago

2.0.3

4 years ago

2.0.2

4 years ago

2.0.1

4 years ago

2.0.0

4 years ago

1.2.6

4 years ago

1.2.5

4 years ago

1.2.4

4 years ago

1.2.3

4 years ago

1.2.2

4 years ago

1.2.1

4 years ago

1.2.0

4 years ago

1.1.3

4 years ago

1.1.2

4 years ago

1.1.1

4 years ago

1.1.0

4 years ago

1.0.9

4 years ago

1.0.8

4 years ago

1.0.7

4 years ago

1.0.6

4 years ago

1.0.5

4 years ago

1.0.4

4 years ago

1.0.3

4 years ago

1.0.2

4 years ago

1.0.1

4 years ago

1.0.0

4 years ago