Build an API-Only Server
Use build an api-only server with the current published Askr packages.
@askrjs/askr0.0.59
How to use build an api-only server
Compose a context-first router, validate every external input, and return explicit response helpers from handlers.
- Import the published @askrjs/askr entrypoint.
- Keep build an api-only server configuration next to the component, route, or server composition root that owns it.
- Handle pending, unavailable, cancellation, and failure states, then verify the production path.
import { createRouter, created, notFound, ok } from '@askrjs/server';
export const router = createRouter()
.get('/projects/:id', async (context) => {
const project = await context.dependencies.projects.get(context.params.id);
return project ? ok(project) : notFound();
})
.post('/projects', async (context) => created(
await context.dependencies.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 in that priority order, with route params always authoritative. 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.