skmtcdocs

@skmtc/gen-express

Produce Express route registrations from an OpenAPI spec.

An operation generator. Useful when you want to scaffold a typed server stub that matches your spec. Demonstrates the shared-singleton pattern with tiny-invariant for instance narrowing.

What it generates

Per tag, a <tag>/routes.generated.ts file registering every operation on a shared Router, delegating each handler to a service function you implement (real output, abridged):

import {Router, Request, Response, NextFunction} from 'express'
import {getPetFindByStatusService, getPetPetIdService} from '@/pet/services.ts'

export const app = Router()

app.get('/pet/findByStatus',  async (req: Request, res: Response, next: NextFunction) => {
  try {
    const {status} = req.query

    const result = await getPetFindByStatusService({params: {status}})()

    if (!result) {
      return res.status(404).send()
    }

    res.json(result)
  } catch (error) {
    next(error)
  }
})

The generated routes never contain business logic: each handler extracts the operation's parameters, calls the matching <operation>Service function, sends 404 on a falsy result, and forwards thrown errors to next. The @/<tag>/services.ts module is the consumer-owned seam — you write the service implementations; the generator wires the HTTP layer around them.

Source

skmtc-generators/gen-express/src/

Key decisions

  • Single shared app Projection. The entry calls findDefinition({ name: 'app', exportPath }) for each operation. If the app exists, append the new route. If not, create it via insertOperation and append. Same pattern as gen-msw's MockRoutesList, but the accumulator carries the framework's router instance, not a list.
  • tiny-invariant for instance narrowing.
    invariant(app?.value instanceof ExpressApp, 'app must be an instance of ExpressApp')
    TypeScript can't narrow findDefinition's return to the specific Projection class — the invariant asserts at runtime and narrows for the compiler. This pattern is reusable for any shared-aggregate generator.
  • TODO comments survive into output. Each generated handler has a // TODO placeholder. Stub-and-edit by design — the generator produces a scaffold, not a working server.

What to learn from it

  • Shared-singleton aggregator pattern. The accumulator is one ExpressApp instance (not an array of routes). Every operation's transform calls app.append(operation) to add its route to the shared instance. The Projection's toString() then renders all accumulated routes.
  • invariant for type narrowing. When findDefinition returns a generic Projection but you need the specific class, tiny-invariant is the canonical narrowing tool. Don't use as casts.
  • Stub-and-edit output. Some generators produce ready-to-deploy code; others (like this one) produce scaffolds the user customizes. Both are valid styles — pick based on what your generator is realistically producing.

Common customizations when cloned

  • Add middleware insertion (auth, logging, validation).
  • Swap Express for another framework (Fastify, Koa) — the shared-singleton pattern is unchanged.
  • Replace TODO placeholders with delegating calls (e.g., app.get('/users/:id', userController.getUser) that imports from a hand-written controller module).
  • Add automatic request-body validation via the Zod schemas from gen-zod.

See also

On this page