Skip to content
On this page

This is a koa-wrapping library for...

  1. making the conventions I use to build RESTful APIs more terse and
  2. adding utilities and middleware that I find useful in all API design.

basic usage

js
const atst = require('@ashnazg/atst');

const {app, api} = atst();
api.get('/status', ctx => { // api is just a koa router prefigured with "/api" stem.
	ctx.body = {status: 'alive'};
});

The above is locally testable at curl -D- -H'Accept: application/json' http://localhost:4423/api/status

public routes

authentication and sessions

Most use cases require stateful or private endpoints; provide hooks for login->account and cookie->session and the kit will create one or more protected routers:

js
const example_ram_session_store = {};
const {api, rolename1} = atst({
	auth: {
		keys: ['one random server-lifespan key'],
		rolename1: {
			async (user, pass) => {
				const client_facing = {uid: 42, name: 'bob'};
				const stored_in_context = {uid: 42, admin: true};
				return {resp: client_facing, store: stored_in_context};
			}
		}
	},
	saveSession(store) {
		const {sid} = store; // kit marshalls that to the cookie and back
		example_ram_session_store[sid] = store;
	},
	loadSession(sid) {
		return example_ram_session_store[sid];
	},
});
api.get('/status', ctx => { // api is just public (including the login/session entry points.)
	ctx.body = {status: 'alive'};
});
rolename1.get('/my-messages', ctx => {
	ctx.body = {msgs: []};
});

factory options

These are the default behavior:

js
const {api, app} = atst({
	port: process.env.PORT || 5000,
	server_id: require('os').hostname() + 'isotimestamp',
	request_size_limit: '1mb', // applies to both json and form-encoded
});

(dev-server / local-only convenience: if you pass falsy as a server_id, it'll shorten the log lines.)

monitoring launch

The factory also returns a promise resolved when the network socket is ready:

js
const {api, app, ready} = atst();
ready.then(() => console.log("listening"));

error tools

While koa provides a fatal-grade hook at ctx.throw, this kit expects multiple warnings and/or errors are possible, so it provides the app ctx.warn and ctx.fail with the same signature as throw. All three hooks affect both the http response status as well as populate an error[] field in the response:

js
api.get('/sekrets', ctx => {
	ctx.fail(403, 'you are not an admin');
	ctx.warn(401, 'you are not logged in');
	ctx.throw(418, "I don't like tuesdays");
	ctx.warn(400, "this code is unreachable.");
});

curl -SsD/dev/stderr -H'Accept: application/json' http://localhost:5173/api/sekrets now results in:

HTTP/1.1 418 I'm a teapot

{
    "errors": [
        {
            "code": 403,
            "msg": "you are not an admin"
        },
        {
            "code": 401,
            "msg": "you are not logged in",
            "severity": "warning"
        },
        {
            "code": 418,
            "msg": "I don't like tuesdays"
        }
    ]
}

Note that the highest code is promoted to the HTTP status line -- that includes prioritizing a 5xx over a 4xx.

internal errors are redacted

Details about 5xx are never sent in the response; instead, an opaque event ID is sent and the details go to the server's stderr:

api.get('/sekrets', ctx => {
	ctx.warn('we forgot to implement auth', {due: "yesterday"});
	ctx.fail(502, 'db is down', {eta: "who knows"});
	ctx.body = {"no soup": "for you"};
	ctx.throw("I can't work under these conditions", {notice: "2wks"});
});

Will show the client only...

HTTP/1.1 502 Bad Gateway

{
    "no soup": "for you",
    "errors": [
        {
            "code": 500,
            "eid": "req0_sernefertari.local2023-08-13T22:58:41.013Z_err0",
            "msg": "see error <req0_sernefertari.local2023-08-13T22:58:41.013Z_err0> in the server logs"
        },
        {
            "code": 502,
            "eid": "req0_sernefertari.local2023-08-13T22:58:41.013Z_err1",
            "msg": "see error <req0_sernefertari.local2023-08-13T22:58:41.013Z_err1> in the server logs"
        },
        {
            "code": 500,
            "eid": "req0_sernefertari.local2023-08-13T22:58:41.013Z_err2",
            "msg": "see error <req0_sernefertari.local2023-08-13T22:58:41.013Z_err2> in the server logs"
        }
    ]
}

...but the server stderr logs will show...

FAIL 500 <req0_sernefertari.local2023-08-13T22:58:41.013Z_err0> we forgot to implement auth {due:'yesterday',severity:'warning'}
FAIL 502 <req0_sernefertari.local2023-08-13T22:58:41.013Z_err1> db is down {eta:'who knows'}
FAIL 500 <req0_sernefertari.local2023-08-13T22:58:41.013Z_err2> I can't work under these conditions {notice:'2wks'}
    at Object.throw (/Users/xander/projects/ashnazg-npm/atst/node_modules/koa/lib/context.js:97:11)
    at /Users/xander/projects/vault/2023/be/src/serve.js:36:11
    at dispatch (/Users/xander/projects/ashnazg-npm/atst/node_modules/koa-compose/index.js:42:32)
    at /Users/xander/projects/ashnazg-npm/atst/node_modules/@koa/router/lib/router.js:372:16
    at dispatch (/Users/xander/projects/ashnazg-npm/atst/node_modules/koa-compose/index.js:42:32)
    at /Users/xander/projects/ashnazg-npm/atst/node_modules/koa-compose/index.js:34:12
    at dispatch (/Users/xander/projects/ashnazg-npm/atst/node_modules/@koa/router/lib/router.js:377:31)
    at dispatch (/Users/xander/projects/ashnazg-npm/atst/node_modules/koa-compose/index.js:42:32)
    at /Users/xander/projects/ashnazg-npm/atst/new-2023-08-13.js:102:10
    at dispatch (/Users/xander/projects/ashnazg-npm/atst/node_modules/koa-compose/index.js:42:32)
502 <req0_sernefertari>.local2023-08-13T22:58:41.013Z GET /api/sekrets 3ms Bad Gateway

controlling log lines

An access.log line is in the format {status} {request_id} {method} {path} {perf_duration} {message}.

Message can be overridden for more useful summaries; set ctx.log to what you want to see after the perf duration:

js
api.get('/status', ctx => {
	ctx.body = {status: 'running'};
	ctx.log = 'pinged by monitoring';
});

results in: 200 <req0> GET /api/status 1ms pinged by monitoring

event ids

If all are available, a request is ID'ed as req$(INT)_sesh$(SESH)_u$(USER_ID)_ser$(SERVER_ID) and errors that happen in that request add _err$(INT). Any factor that's not available is skipped, so with {server_id: 0} and no auth/sessions in play, requests are merely req421 and errors req421_err0.

throws

throwing numbers, strings or errors are similarly handled:

  1. throw 'foo' is the same as ctx.throw(500, 'foo')
  2. throw 408 is the same as ctx.throw(408, 'Request Timeout')
  3. throw new Error('foo') (optionally, with props) is unpacked into a ctx.throw(). (If you set ether code or status, that'll feed the http-status-based fields.)

sessions

check out https://github.com/koajs/generic-session

JavaScript/Bash source released under the MIT License.