New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

kequapp

Package Overview
Dependencies
Maintainers
1
Versions
65
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

kequapp

Versatile, non-intrusive, tiny webapp framework

  • 0.0.3
  • Source
  • npm
  • Socket score

Version published
Weekly downloads
16
decreased by-70.91%
Maintainers
1
Weekly downloads
 
Created
Source

Kequapp

This is the development branch of a request listener for nodejs web apps.

It's intended to be versatile, non-intrusive, and make working with node's server capabilities easier without changing built in functionality.

Simple Setup

npm i kequapp
const { createServer } = require('http');
const { createApp } = require('kequapp');

const app = createApp();

app.route('/', () => {
    return 'Hello world!';
});

createServer(app).listen(4000, () => {
    console.log('Server running on port 4000');
});

Routing

Routes are defined using route(). Method is optional default is 'GET', path is optional default is '/', followed by any number of handlers which define the request lifecycle.

Branches are defined using branch(). Path prefix is optional default is '/', followed by any number of handlers. It returns a branch of the application which will adopt all handlers and use the given path prefix. By itself this does not create a route, it will be used in conjunction with routes.

Handlers are added to the current branch using middleware(). Provide any number of handlers you would like that will affect all route and branch siblings. This is most useful on the app instance itself.

const { Ex } = require('kequapp');
function json ({ res }) {
    res.setHeader('Content-Type', 'application/json; charset=utf-8');
}

function loggedIn ({ req, context }) {
    if (req.headers.authorization !== 'mike') {
        throw Ex.Unauthorized();
    }
    context.auth = req.headers.authorization;
}

app.branch('/user')
    .middleware(json)
    .route(() => {
        return { result: [] };
    })
    .route('/:id', ({ params }) => {
        return { userId: params.id };
    });

app.route('/admin/dashboard', loggedIn, ({ context }) => {
    return `Hello admin ${context.auth}!`;
});

Renderers

Default renderers are included for text/plain, and application/json. Renderers are chosen based on the Content-Type header set by your application. The above example would cause all routes of the /user branch to trigger the application/json renderer.

You can override renderers or add your own by defining renderers. These act as the final step of a request's lifecycle and should explicitly finalize the response.

const app = createApp({
    renderers: {
        'text/html': (payload, { res }) => {
            const html = myMarkupRenderer(payload);
            res.end(html);
        }
    }
});

Halting Execution

Any handler can return a payload. Doing this halts further execution of the request and triggers rendering immediately. This is similar to interrupting the request by throwing an error or by finalizing the response.

All processing halts if the response has been finalized. This is useful for example instead of rendering output you want to redirect the user to another page.

function members({ req, res }) {
    // must be authenticated!
    if (!req.headers.authorization) {
        res.statusCode = 302;
        res.setHeader('Location', '/login');
        // finalize response
        res.end();
    }
}

const membersBranch = app.branch('/members', members);

Parameters

The following parameters are made available to handlers and renderers.

parameterdescription
reqThe node req object.
resThe node res object.
urlUrl requested by the client.
contextParams shared between handler functions.
paramsParams extracted from the pathname.
queryParams extracted from the querystring.
getBodyFunction to extract params from the request body.
loggerLogger specified during setup.

Body

Node delivers the body of a request in chunks. It is not always necessary to wait for the request to finish before we begin processing it. Therefore a helper method getBody() is provided which you may use to await body parameters from the completed request.

app.route('POST', '/user', async ({ getBody }) => {
    const body = await getBody();

    // body ~= {
    //     name: 'april'
    // }

    return `User creation ${body.name}!`;
});

Multipart/Raw Body

By passing multipart the function will return both a body and files.

app.route('POST', '/user', async ({ getBody }) => {
    const [body, files] = await getBody({ multipart: true });

    // body ~= {
    //     name: 'april'
    // }
    // files ~= [{
    //     headers: {
    //         'content-disposition': 'form-data; name="avatar" filename="my-cat.png"',
    //         'content-type': 'image/png;'
    //     },
    //     mime: 'image/png',
    //     name: 'avatar',
    //     filename: 'my-cat.png',
    //     data: Buffer <...>
    // }]

    return `User creation ${body.name}!`;
});

By passing raw the body is processed as minimally as possible, returning a single buffer as it arrived. When combined with multipart, an array is returned with all parts as separate buffers with respective headers.

app.route('POST', '/user', async ({ getBody }) => {
    const parts = await getBody({ raw: true, multipart: true });

    // parts ~= [{
    //     headers: {
    //         'content-disposition': 'form-data; name="name"'
    //     },
    //     data: Buffer <...>
    // }, {
    //     headers: {
    //         'content-disposition': 'form-data; name="avatar" filename="my-cat.png"',
    //         'content-type': 'image/png;'
    //     },
    //     data: Buffer <...>
    // }]

    return `User creation ${parts[0].data.toString()}!`;
});

Body Normalization

The getBody() helper method allows you to specify which fields should be an array and which fields are required. This is because the server only knows a field should be an array if it received more than one. Required ensures that the field is not null or undefined. More control is offered using validate(). Post processing may be applied using postProcess().

Note these options are ignored when the raw option is used.

function validate (result) {
    if (result.ownedPets.length > 99) {
        return 'Too many pets';
    }
    if (result.ownedPets.length < 1) {
        return 'Not enough pets!';
    }
}

function postProcess (result) {
    return {
        ...result,
        name: result.name.trim(),
        eyeColor: 'blue'
    };
}

app.route('POST', '/user', async ({ getBody }) => {
    const body = await getBody({
        array: ['ownedPets'],
        required: ['name'],
        validate,
        postProcess
    });

    // body ~= {
    //     ownedPets: ['cat'],
    //     name: 'april',
    //     eyeColor: 'blue'
    // }
});

Cookies

I recommend use of an external library for this.

const cookie = require('cookie'); // npm i cookie
app.middleware(({ req, context }) => {
    const cookies = cookie.parse(req.headers.cookie);
    // cookies ~= { myCookie: 'hello' }
    context.cookies = cookies;
});

app.route('/login', ({ res }) => {
    res.setHeader('Set-Cookie', [
        cookie.serialize('myCookie', 'hello')
    ]);
});

Exceptions

Error generation is available by importing the Ex utility. Any thrown error will be caught by the error handler and return a 500 status code, this utility enables you to easily utilize all status codes 400 and above.

These methods create errors with correct stacktraces there is no need to use new.

const { Ex } = require('kequapp');
app.route('/throw-error', () => {
    throw Ex.StatusCode(404);
    throw Ex.StatusCode(404, 'Custom message', { extra: 'info' });
    // same as
    throw Ex.NotFound();
    throw Ex.NotFound('Custom message', { extra: 'info' });
});

Exception Handling

The default error handler returns a json formatted response containing helpful information for debugging. It can be overridden by defining an errorHandler during instantiation. The returned value will be sent to the renderer again for processing.

Errors thrown inside of the error handler or within the renderer chosen to parse the error handler's payload will cause a fatal exception.

This example sends a very basic custom response.

const app = createApp({
    errorHandler (error, { res }) {
        const statusCode = error.statusCode || 500;

        res.statusCode = statusCode;
        res.setHeader('Content-Type', 'text/plain; charset=utf-8');

        return `${statusCode} ${error.message}`;
    }
});

Static Files

A rudimentary staticFiles() handler can be used to deliver files relative to your project directory. This utility makes use of the wildcards parameter as defined by your route to build a valid path.

By default the /public directory is used.

const { staticFiles } = require('kequapp');
app.route('/assets/**', staticFiles({
    dir: '/my-assets-dir',
    exclude: ['/my-assets-dir/private']
}));

If more control is needed a similar sendFile() helper is available.

const { sendFile } = require('kequapp');
app.route('/db.json', async function ({ req, res }) {
    const pathname = '/db/my-db.json';
    await sendFile(req.method, res, pathname);
});

Unit Tests

It is possible to test your application without spinning up a server using the inject() tool. The first parameter is your app, then a config override for your app, followed by options largely used to populate the request.

Returned req and res objects are from the npm mock-req and mock-res modules respectively. Ensure you have both mock-req and mock-res installed in your project.

It also returns getResponse() which is a utility you may use to wait for your application to respond. Alternatively you may inspect what your application is doing in realtime using the req, and res objects manually.

const assert = require('assert');
const { inject } = require('kequapp/test');
it('reads the authorization header', async function () {
    const { getResponse, res } = inject(app, { logger }, {
        url: '/admin/dashboard',
        headers: {
            Authorization: 'mike'
        }
    });

    const body = await getResponse();

    assert.strictEqual(res.getHeader('Content-Type'), 'text/plain; charset=utf-8');
    assert.strictEqual(body, 'Hello admin mike!');
});

A body parameter can optionally be provided for ease of use. All requests are automatically finalized when they are initiated with inject() unless you set body to null. Doing so will allow you to write to the stream.

The following two examples are the same.

it('reads the body of a request', async function () {
    const { getResponse, res } = inject(app, { logger }, {
        method: 'POST',
        url: '/user',
        headers: {
            'Content-Type': 'application/json; charset=utf-8'
        },
        body: '{ "name": "april" }'
    });

    const body = await getResponse();

    assert.strictEqual(body, 'User creation april!');
});

it('reads the body of a request', async function () {
    const { getResponse, req, res } = inject(app, { logger }, {
        method: 'POST',
        url: '/user',
        headers: {
            'Content-Type': 'application/json; charset=utf-8'
        },
        body: null
    });

    req.end('{ "name": "april" }');

    const body = await getResponse();

    assert.strictEqual(body, 'User creation april!');
});

Keywords

FAQs

Package last updated on 22 Oct 2021

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc