Skip to Content
🎉 Try out our new interactive playground

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

First install the CLI and the required runtime packages to your project:

npm i --dev @nahkies/openapi-code-generator @types/koa @types/koa__router npm i @nahkies/typescript-koa-runtime @koa/cors @koa/router koa koa-body zod

See also quick start guide

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 a createRouter and bootstrap function, along with associated types used to create your server
  • models.ts - exports typescript types for schemas
  • schemas.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, })

Multiple routers

By default, a single router is generated, but for larger API surfaces this can become unwieldy.

You can split the generated routers by using the —grouping-strategy option to control the strategy to use for splitting output into separate files. Set to none for a single generated.ts file, one of:

  • none: don’t split output, yield single generated.ts (default)
  • first-tag: group operations based on their first tag
  • first-slug: group operations based on their first route slug/segment

This can help to organize your codebase and separate concerns.

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 } }

Escape Hatches - raw ctx handling

For most JSON based API’s you shouldn’t need to reach for this, but there are sometime situations where the code generation tooling doesn’t support something you need (see also roadmap / compatibility).

Eg: response headers are not yet supported.

To account for these situations, we pass the raw koa ctx object and next function to your handler implementations, allowing you full control where its needed.

const createTodoList: CreateTodoList = async ({body}, respond, ctx, next) => { ctx.set("x-ratelimit-remaining", "100") // ...your implementation return respond.with200().body({ /* ... */ }) }

It’s also possible to skip response processing if needed by returning SkipResponse from your implementation. This allows you take complete control of the response.

import {SkipResponse} from '@nahkies/typescript-koa-runtime/server' const getProfileImage: GetProfileImage = async ({body}, respond, ctx, next) => { ctx.set("x-ratelimit-remaining", "100") ctx.status =200 ctx.body = Buffer.from([/*some binary file*/]) return SkipResponse }

It should be seldom that these escape hatches are required, and overtime fewer and fewer situations will require them.

Last updated on