This is a koa-wrapping library for...
- making the conventions I use to build RESTful APIs more terse and
- adding utilities and middleware that I find useful in all API design.
basic usage
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:
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:
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:
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:
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:
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:
throw 'foo'
is the same asctx.throw(500, 'foo')
throw 408
is the same asctx.throw(408, 'Request Timeout')
throw new Error('foo')
(optionally, with props) is unpacked into a ctx.throw(). (If you set ethercode
orstatus
, that'll feed the http-status-based fields.)
sessions
check out https://github.com/koajs/generic-session