Askr documentation
Guides

Build an API-Only Server

Build an API-Only Server: a worked guide from route registry through to a production build.

Example

Compose a context-first router, validate every external input, and return explicit response helpers from handlers.

import { createRouter } from '@askrjs/server/router';

export const router = createRouter()
  .get('/projects/{id}', async (context) => {
    const project = await projects.get(context.params.id);
    return project ? context.ok(project) : context.notFound();
  })
  .post('/projects', async (context) => context.created(
    await projects.create(await context.bind<CreateProjectInput>())
  ));

Goal and architecture

@askrjs/server is a transport-neutral HTTP layer built directly on Web Request/Response, with no router-owned UI concerns — appropriate when you're shipping a JSON or OpenAPI backend with no Askr client attached. createRouter() plus createServerApp({ router }) is the whole surface; literal route paths like /objects/{*key} infer their param types automatically, including named wildcards for capturing remaining path segments.

Implementation

Use ctx.bind<T>() to read the request body once into a typed model — it merges JSON/form body, query string, and route params, with route params overriding a same-named query value and query overriding a same-named body value, so precedence runs params, then query, then body, highest to lowest. For a published contract, build routes through @askrjs/server/openapi's createApi() instead of the bare router: api.group('/api/users').get('/{id}', handler).operationId(...).summary(...) gives you the handler, dependency injection, and the OpenAPI document from one fluent definition, and askr openapi --check verifies the generated spec hasn't drifted. WebSocket routes are declared the same way with router.ws(path, handler), with the runtime-specific upgrade supplied separately through a WebSocketAdapter.

Failure states

Malformed JSON, a non-object JSON root, or an already-consumed body from ctx.bind() all produce a 400 application/problem+json response automatically — you don't need to hand-roll that check. Auth failures should go through ctx.challenge() to emit a correct WWW-Authenticate header rather than a bare 401. Wire the built-in /livez, /readyz, /startupz, and /targetz probes to real dependency checks (probes.readyz, probes.startupz, probes.targetz on createServerApp) so orchestrators can tell a genuinely broken instance from one that's still booting.

Verification

Hit each probe path directly and confirm they return 503 when a dependency check returns false or throws, and 200 otherwise — this is easy to get backwards. Send a request with a duplicate query key and confirm ctx.bind() produces the expected array rather than silently dropping one value. If you generated an OpenAPI document, run askr openapi --check in CI so a handler change that alters the contract fails the build instead of shipping quietly.