@nahkies/openapi-code-generator
TypeScript icon, indicating that this package has built-in type declarations

0.9.0 • Public • Published

@nahkies/openapi-code-generator

CI/CD npm

Generate typescript client SDK's and API server scaffolding (routing, validation, serialization) from OpenAPI 3 specifications.

This package should be considered alpha quality. However, as shown by the integration tests, it does a fair job of generating a strongly typed client for large/complex definitions like the GitHub api.

Project Goal

To make it fun, easy and productive to generate both client and server "glue" code from openapi 3 definitions. This is code that is tedious and error prone to maintain by hand, by automating it we can reduce toil and frustration.

The generated code output should be "stable" in the sense that it will not arbitrarily change between generation without need (for a given version). For example outputting entities in alphabetic order where possible.

It should also be generated in a way that human would reasonably write it, the intent is that the generated code can and should be committed to the consuming project repositories, allowing it to be reviewed, and audited overtime.

This is particularly useful in the case of mistakes in the generation or schemas

The initial focus on typescript, with an intention to later support other languages. kotlin is the most likely candidate for a second language.

Usage

Install as a devDependency in the consuming project, or execute using npx

yarn add --dev @nahkies/openapi-code-generator

See available options using:

yarn openapi-code-generator --help

Or looking at the code defining them in index.ts. All options can be provided as either cli arguments or environment variables.

Example usage:

yarn openapi-code-generator --input="./openapi.yaml" --out="./src/" --template=typescript-koa

Where template is one of:

  • typescript-angular
  • typescript-fetch
  • typescript-koa

There is an optional parameter schema-builder for choosing between:

For runtime phrasing / validation of schemas (eg: responses, parameters).

Client Examples

There are two client templates:

  • typescript-fetch
  • typescript-angular

Typescript Fetch

The typescript-fetch template outputs a client SDK based on the fetch api that gives the following:

  • Typed methods to call each endpoint
  • Support for passing a timeout, abort signals are still respected

It does not yet support runtime validation/parsing - compile time type safety only at this stage.

See integration-tests/typescript-fetch for more samples.

Dependencies:

yarn add @nahkies/typescript-fetch-runtime

If you're using a version of NodeJS that doesn't include the fetch API, you may need a polyfill like node-fetch

Running:

yarn openapi-code-generator \
  --input ./openapi.yaml \
  --output ./src/clients/some-service \
  --template typescript-fetch \
  --schema-builder zod

Will output these files into ./src/clients/some-service:

  • ./client.ts: exports a class ApiClient that implements methods for calling each endpoint
  • ./models.ts: exports typescript types
  • ./schemas.ts: exports runtime parsers using the chosen schema-builder (default zod)

Once generated usage should look something like this:

const client = new ApiClient({
  basePath: `http://localhost:${address.port}`,
  defaultHeaders: {
    "Content-Type": "application/json",
    Authorisation: "Bearer: <TOKEN>", // can pass auth headers here
  },
})

const res = await client.createTodoListItem({
  listId: list.id,
  requestBody: {content: "test item"},
  // optionally pass a timeout (ms), or any arbitrary fetch options (eg: an abort signal)
  // timeout?: number,
  // opts?: RequestInit
})

// checking the status code narrows the response body types (ie: remove error types from the type union)
if (res.status !== 200) {
  throw new Error("failed to create item")
}

// body will be typed correctly
const body = await res.json()
console.log(`id is: ${body.id}`)

Typescript Axios

The typescript-axios template outputs a client SDK based on the axios that gives the following:

  • Typed methods to call each endpoint

It does not yet support runtime validation/parsing - compile time type safety only at this stage.

It follows the standard axios pattern of rejecting any response that isn't 2xx and thus can't provide typed error responses. If you'd like to have strong typing over your error responses consider using the typescript-fetch template.

See integration-tests/typescript-axios for more samples.

Dependencies:

yarn add axios @nahkies/typescript-axios-runtime

Running:

yarn openapi-code-generator \
  --input ./openapi.yaml \
  --output ./src/clients/some-service \
  --template typescript-axios \
  --schema-builder zod

Will output these files into ./src/clients/some-service:

  • ./client.ts: exports a class ApiClient that implements methods for calling each endpoint
  • ./models.ts: exports typescript types
  • ./schemas.ts: exports runtime parsers using the chosen schema-builder (default zod)

Once generated usage should look something like this:

const client = new ApiClient({
  // Pass a axios instance if you wish to use interceptors for auth, logging, etc
  // axios: axios.create({...}),
  basePath: `http://localhost:${address.port}`,
  defaultHeaders: {
    "Content-Type": "application/json",
    Authorisation: "Bearer: <TOKEN>", // can pass auth headers here
  },
})

// rejects if status code isn't 2xx, following typical axios behavior
const res = await client.createTodoListItem({
  listId: list.id,
  requestBody: {content: "test item"},
  // optionally pass a timeout (ms), or any arbitrary axios options
  // timeout?: number,
  // opts?: AxiosRequestConfig
})

// data will be typed correctly
console.log(`id is: ${res.data.id}`)

Typescript Angular

Note: this is the least battle tested of the templates and most likely to have critical bugs

The typescript-angular template outputs a client SDK based on the Angular HttpClient that gives the following:

It does not yet support runtime validation/parsing - compile time type safety only at this stage.

See integration-tests/typescript-angular for more samples.

Running:

yarn openapi-code-generator \
  --input ./openapi.yaml \
  --output ./src/app/clients/some-service \
  --template typescript-angular \
  --schema-builder zod

Will output these files into ./src/app/clients/some-service:

  • ./api.module.ts: exports a class ApiModule as an NgModule
  • ./client.service.ts: exports a class ApiClient as injectable Angular service
  • ./models.ts: exports typescript types
  • ./schemas.ts: exports runtime parsers using the chosen schema-builder (default zod)

Once generated usage should look something like this:

// Root Angular module
@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, AppRoutingModule, ApiModule],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"],
})
export class AppComponent {
  // inject into your component
  constructor(client: ApiClient) {
    client.updateTodoListById({listId: "1", requestBody: {name: "Foo"}}).subscribe((next) => {
      if (next.status === 200) {
        // TODO: body is currently incorrectly `unknown` here
        console.log(next.body.id)
      }
    })
  }
}

Server Examples

Currently, there is a single server template: typescript-koa

Support for express or other frameworks may be added in future.

Typescript Koa

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 handlers 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.

Dependencies:

yarn add @nahkies/typescript-koa-runtime @koa/cors @koa/router koa koa-body zod
yarn add --dev @types/koa @types/koa__router

Running:

yarn openapi-code-generator \
  --input ./openapi.yaml \
  --output ./src \
  --template typescript-koa \
  --schema-builder zod

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 intellisense experience.
  // the body is additionally validated against the response schema/status code at runtime
  return respond.with200().body(dbListToApiList(list))
  // 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 / configuration

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.

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

More information / contributing

Refer to top level README.md / CONTRIBUTING.md

Package Sidebar

Install

npm i @nahkies/openapi-code-generator

Weekly Downloads

110

Version

0.9.0

License

MIT

Unpacked Size

3.35 MB

Total Files

216

Last publish

Collaborators

  • mnahkies