Using the typescript-koa
template
The typescript-koa
template outputs scaffolding code that handles the following:
- Building a @koa/router instance with all routes in the openapi specification
- Generating types and runtime schema parsers for all request parameters/bodies and response bodies
- Generating types for route implementations that receive validated inputs, and have return types that are additionally validated at runtime prior to sending the response
- (Optionally) Actually starting the server and binding to a port
See integration-tests/typescript-koa for more samples.
Install dependencies
Prerequisite installed the cli
npm i @nahkies/typescript-koa-runtime @koa/cors @koa/router koa koa-body zod
npm i --dev @types/koa @types/koa__router
Run generation
npm run openapi-code-generator \
--input ./openapi.yaml \
--input-type openapi3 \
--output ./src \
--template typescript-koa \
--schema-builder zod
Using the generated code
Running the above will output three files into ./src
:
generated.ts
- exports acreateRouter
andbootstrap
function, along with associated types used to create your servermodels.ts
- exports typescript types for schemasschemas.ts
- exports runtime schema validators
Once generated usage should look something like this:
import {bootstrap, createRouter, CreateTodoList, GetTodoLists} from "../generated"
// Define your route implementations as async functions implementing the types
// exported from generated.ts
const createTodoList: CreateTodoList = async ({body}, respond) => {
const list = await prisma.todoList.create({
data: {
// body is strongly typed and parsed at runtime
name: body.name,
},
})
// (recommended) the respond parameter is a strongly typed helper that
// provides a better editor experience.
// the response body is additionally validated against the response schema/status code at runtime
return respond.with200().body(dbListToApiList(list))
// (deprecated) alternatively, you can return a {status, body} object which is also strongly typed
// pattern matching the status code against the response schema:
// return {
// status: 200 as const,
// body: dbListToApiList(list)
// }
}
const getTodoLists: GetTodoLists = async ({query}) => {
// omitted for brevity
}
// Starts a server listening on `port`
bootstrap({
router: createRouter({getTodoLists, createTodoList}),
port: 8080,
})
Custom Koa app/config
The provided bootstrap
function has a limited range of options. For more advanced use-cases,
such as https
you will need to construct your own Koa app
, and mount the router returned by createRouter
.
The only real requirement is that you provide a body parsing middleware before the router
that places
a parsed request body on the ctx.body
property.
Eg:
import {createRouter} from "../generated"
import KoaBody from "koa-body"
import https from "https"
// ...implement routes here
const app = new Koa()
// it doesn't have to be koa-body, but it does need to put the parsed body on `ctx.body`
app.use(KoaBody())
// mount the generated router
const router = createRouter({getTodoLists, createTodoList})
app.use(router.allowedMethods())
app.use(router.routes())
https
.createServer(
{
key: "...",
cert: "...",
},
app.callback(),
)
.listen(433)
Error Handling
Any errors thrown during the request processing will be wrapped in KoaRuntimeError
objects,
and tagged with the phase
the error was thrown.
interface KoaRuntimeError extends Error {
cause: unknown // the originally thrown exception
phase: "request_validation" | "request_handler" | "response_validation"
}
This allows for implementing catch-all error middleware for common concerns like failed request validation, or internal server errors.
Eg:
export async function genericErrorMiddleware(ctx: Context, next: Next) {
try {
await next()
} catch (err) {
// if the request validation failed, return a 400 and include helpful
// information about the problem
if (KoaRuntimeError.isKoaError(err) && err.phase === "request_validation") {
ctx.status = 400
ctx.body = {
message: "request validation failed",
meta: err.cause instanceof ZodError ? {issues: err.cause.issues} : {},
} satisfies t_Error
return
}
// return a 500 and omit information from the response otherwise
logger.error("internal server error", err)
ctx.status = 500
ctx.body = {
message: "internal server error",
} satisfies t_Error
}
}