RestNio

# In your node project folder run the command:
npm i restnio

About

RestNio is a Node.js server framework built around one idea: write a route once, and it works over both HTTP and WebSocket automatically. No duplicate handlers and no glue code!
On top of bimodal routing, RestNio brings built-in parameter validation & type coercion, a declarative JWT permission system, static-file & CORS plugins, outbound HTTP/WebSocket connectors, and full TypeScript support, all without many external dependencies.
RestNio started as a hobby project in 2018 and has since powered everything from personal websites to IoT admin panels, conferencing software, and online games. It is similar in spirit to Express.js but takes a different angle: a regex-first routing engine and a unified transport model that keeps REST and real-time code in the same place.

Examples

const RestNio = require('restnio');

const app = new RestNio((router) => {
    router.get('/', () => 'Hello from RestNio');
}, {
    port: 7070
});

app.bind();
const RestNio = require('restnio');

const app = new RestNio((router, rnio) => {

    // A string response is sent as text/html; an object becomes JSON.
    router.get('/', () => ({ status: 'RestNio is running' }));

    // Path parameters land in params just like query/body values.
    router.get('/greet/:name', (params) => `Hello, ${params.name}!`);

    // Bimodal by default — the same handler works for HTTP GET
    // and for WebSocket clients that send { "path": "/ping" }.
    router.get('/ping', () => ({ pong: true }));

    // HTTP-only variant (WebSocket clients are rejected).
    router.httpGet('/http-only', () => 'Only over HTTP');

    // WebSocket-only route.
    // WS clients send: { "path": "/ws-only", "params": {} }
    router.ws('/ws-only', () => 'Only over WebSocket');

    // Nested router — keeps large APIs organised in separate files.
    router.use('/v1', require('./routes/v1'));

}, { port: 7070 });

app.bind();
const RestNio = require('restnio');

const app = new RestNio((router, rnio) => {
    const { params } = rnio;

    // Shorthand helpers — type coercion + required check in one line.
    router.post('/register', {
        params: {
            username: params.string,
            age:      params.integer,
            email:    params.email,
            role:     params.enum('user', 'admin')
        },
        func: (p) => ({ ok: true, user: p })
    });

    // Full notation — custom checks, formatters, and optional defaults.
    router.post('/search', {
        params: {
            query: {
                required:   true,
                type:       'string',
                checks:     [(s) => s.length >= 3],
                formatters: [(s) => s.trim()]
            },
            page: { type: 'number', default: 1 }
        },
        func: (p) => ({ results: [], query: p.query, page: p.page })
    });

    // Validated arithmetic — a and b are guaranteed numbers.
    router.post('/sum', {
        params: { a: params.number, b: params.number },
        func: ({ a, b }) => ({ result: a + b })
    });

}, { port: 7070 });

app.bind();
const RestNio = require('restnio');

const app = new RestNio((router, rnio) => {

    // Issue a JWT token containing a list of permissions.
    router.get('/login', () =>
        rnio.token.grant(['dogs.read', 'dogs.feed.fido'])
    );

    // Route requires the 'dogs.read' permission.
    router.get('/dogs', {
        permissions: ['dogs.read'],
        func: () => ({ dogs: ['fido', 'rex'] })
    });

    // Permission template — :name is substituted from the URL.
    // A token with 'dogs.feed.fido' can only feed fido, not rex.
    router.post('/dogs/feed/:name', {
        permissions: ['dogs.feed.:name'],
        func: (params) => ({ fed: params.name })
    });

    // Cookie-based login — the token cookie is picked up automatically
    // on every subsequent HTTP request and WebSocket upgrade.
    router.get('/cookielogin', async (params, client) => {
        client.cookie('token',
            await rnio.token.grant(['admin'], { expiresIn: '1h' })
        );
        return 'Logged in!';
    });

}, {
    port: 7070,
    auth: { secret: 'change-me-in-production' }
});

app.bind();
const RestNio = require('restnio');

// Routes are bimodal — HTTP and WebSocket share the same handler.
// WS clients send a JSON envelope: { "path": "/echo", "params": { "msg": "hi" } }
// and receive the return value as JSON.

const app = new RestNio((router, rnio) => {

    // Reachable via HTTP GET and WebSocket alike.
    router.get('/echo', (params) => ({ echo: params.msg }));

    // Subscribe a WebSocket client to the 'news' channel.
    router.ws('/subscribe', async (params, client) => {
        await client.subscribe('news');
        return { subscribed: true };
    });

    // Push a headline to every subscriber (works over HTTP too).
    router.post('/broadcast', async (params) => {
        await rnio.subs().publish('news', { headline: params.msg });
        return { sent: true };
    });

    // Binary WebSocket route — receives a raw Buffer.
    router.wsBin('/upload', (params, client) => {
        return { received: client.binary.length };
    });

}, { port: 7070 });

app.bind();
import RestNio from 'restnio';

const app = new RestNio((router, rnio) => {

    // Path param :name is inferred as string — no cast needed.
    router.get('/dog/:name', (params) => {
        return { dog: params.name };
    });

    // Type literal gives params.age type number at compile time.
    router.post('/register', {
        params: {
            username: rnio.params.string,
            age:      { required: true, type: 'number' as const }
        },
        func: (params) => {
            const age: number = params.age;
            return { ok: true, age };
        }
    });

    // Path params and schema params combine in one typed signature.
    router.post('/dog/:name/feed', {
        params: {
            portion: { required: true, type: 'number' as const }
        },
        func: (params) => {
            const name: string = params.name;    // from :name in path
            const amt:  number = params.portion; // from schema
            return { fed: name, amount: amt };
        }
    });

}, { port: 7070 });

app.bind();

Tutorials