# openapi-ts-hono

Type-check a `Hono` app against an OpenAPI `paths` type generated by [`openapi-typescript`](https://openapi-ts.dev/).

`openapi-ts-hono` keeps normal Hono route definitions as the authoring API, then
uses TypeScript to catch drift from your OpenAPI contract. This is especially
useful when humans and AI coding agents both edit handlers: they can keep writing
plain Hono code, while mismatched paths, params, validators, status codes, content
types, and response bodies surface as readable compiler errors, with no runtime overhead.

## Installation

```sh
pnpm add hono
pnpm add -D openapi-ts-hono openapi-typescript typescript
```

When using `openapi-ts-hono`, include the DOM library in your TypeScript
configuration:

```json
{
  "compilerOptions": {
    "lib": ["ESNext", "DOM"]
  }
}
```

## Usage

`defineApp` is a type-level assertion. It returns the same Hono app at runtime,
but TypeScript checks that the app implements the routes described by the
OpenAPI `paths` type.

Assume `openapi-typescript` generated a `paths` type like this:

```ts
export interface paths {
  "/users/{id}": {
    get: {
      parameters: {
        path: { id: string };
      };
      responses: {
        200: {
          content: {
            "application/json": { id: string; name: string };
          };
        };
      };
    };
  };
}
```

Then a matching Hono app type-checks:

```ts
import { Hono } from "hono";
import { defineApp } from "openapi-ts-hono";

// generated by openapi-typescript
import type { paths } from "./openapi-types";

const app = defineApp<paths>()(
  new Hono().get("/users/:id", (c) => {
    return c.json({ id: c.req.param("id"), name: "Alice" });
  }),
);
```

If you prefer to reuse the OpenAPI binding, keep the curried function:

```ts
const defineAppWithPaths = defineApp<paths>();

const app = defineAppWithPaths(
  new Hono().get("/users/:id", (c) => {
    return c.json({ id: c.req.param("id"), name: "Alice" });
  }),
);
```

For sub apps, pass the mounted base path as the second type parameter:

```ts
const userApp = defineApp<paths, "/users">()(
  new Hono().get("/:id", (c) => {
    return c.json({ id: c.req.param("id"), name: "Alice" });
  }),
);
const routedApp = defineApp<paths>()(new Hono().route("/users", userApp));

export default app;
```

Extra routes are allowed. The check focuses on whether the OpenAPI routes are
implemented with compatible path parameters, validated inputs, response status
codes, content types, and response bodies.

### Allowing middleware-handled response statuses

By default, every response status in the OpenAPI operation must be represented
by the route handler. If a middleware handles common error responses such as
`404` or `500`, mark those statuses as optional with the type-level
`WithOptionalResponseStatuses` helper:

```ts
import { Hono } from "hono";
import { defineApp, type WithOptionalResponseStatuses } from "openapi-ts-hono";

import type { paths } from "./openapi-types";

type AppPaths = WithOptionalResponseStatuses<paths, 404 | 500>;

const app = defineApp<AppPaths>()(
  new Hono().get("/users/:id", (c) => {
    return c.json({ id: c.req.param("id"), name: "Alice" });
  }),
);
```

## Error Examples

If the app does not conform to the OpenAPI schema, TypeScript raises an error on
the `defineApp<paths>()(...)` call. The error contains a readable key that points
to the mismatch.

Missing route or method:

```ts
defineApp<paths>()(
  new Hono().post("/users/:id", (c) => {
    return c.json({ id: c.req.param("id"), name: "Alice" });
  }),
);
// Type error includes:
// "GET /users/:id is missing"
```

Path parameter name mismatch:

```ts
defineApp<paths>()(
  new Hono().get("/users/:userId", (c) => {
    return c.json({ id: c.req.param("userId"), name: "Alice" });
  }),
);
// Type error includes:
// "Path input mismatch at GET /users/:id"
```

Response body mismatch:

```ts
defineApp<paths>()(
  new Hono().get("/users/:id", (c) => {
    return c.json({ userId: c.req.param("id") });
  }),
);
// Type error includes:
// "Output mismatch at GET /users/:id for 200 application/json"
```

Required JSON request body without a validator:

```ts
type createUserPaths = {
  "/users": {
    post: {
      requestBody: {
        content: {
          "application/json": { name: string };
        };
      };
      responses: {
        201: {
          content: {
            "application/json": { id: string; name: string };
          };
        };
      };
    };
  };
};

defineApp<createUserPaths>()(
  new Hono().post("/users", (c) => {
    return c.json({ id: "1", name: "Alice" }, 201);
  }),
);
// Type error includes:
// "JSON input mismatch at POST /users"
```

Optional request bodies do not require a validator, but required request bodies
do. Add a Hono validator middleware when the OpenAPI operation requires JSON,
form, query, header, or cookie input.

## License

MIT License
