How-to guidesGenerate a client and a server

Generate a client and a server

This guide generates both sides of an API from its OpenAPI document, openapi.yaml: a TypeScript client for the Fetch API and an Express server. The same steps work for the other targets of apigenie-generator, in Java, Go, Python, C# and Elixir, and for AsyncAPI, GraphQL and Protobuf documents.

You need the CLI installed; the Quickstart shows how. Lint the document first (Lint and format API descriptions): a clean contract gives cleaner code.

Generate the client

apigenie-generator client fetch openapi.yaml -o client --package-name shipping-client

The output is an npm package with no runtime dependencies: requests go through the global fetch(), so the client runs in browsers and in Node.js 18 or later. Each operation is an async function named after its operationId, with its parameters and its result typed from the document:

import { configure, ApiError, listShipments, getShipment } from "shipping-client";

configure({
  baseUrl: "https://api.example.com/v2",
  auth: { apiKey: "ce_live_123" },
});

try {
  const page = await listShipments({ status: ["in_transit"], limit: 20 });
  const shipment = await getShipment({ shipmentId: "shp_01J9Z" });
} catch (error) {
  if (error instanceof ApiError) {
    console.error(error.status, error.body);
  }
}

To compile the client as part of your own project instead of as a package, set layout: sources for client_fetch in the configuration (see Configure the generators below): the generator then writes only the sources.

Generate the server

apigenie-generator server express openapi.yaml -o server --package-name shipping-server

The generator writes typed handler interfaces and a router that decodes and validates every request. Your code implements the handlers in files of its own, so regenerating never overwrites it:

import express from "express";
import { BASE_PATH, createRouter, HttpError, type ShipmentsHandlers } from "shipping-server";

const shipments: Partial<ShipmentsHandlers> = {
  async getShipment(request) {
    const shipment = await db.shipments.find(request.params.shipmentId);
    if (shipment === undefined) {
      throw new HttpError(404, "No such shipment");
    }
    return { status: 200, body: shipment };
  },
};

const app = express();
app.use(BASE_PATH, createRouter({ shipments }));
app.listen(8080);

Operations are grouped by their first tag (ShipmentsHandlers), and the status, body and headers a handler returns are typed per status from the document. A change to the contract that the handlers don’t follow is a compile error.

Configure the generators

Generator settings go in the commands.generate section of apigenie.yaml, one entry per target, so the command lines stay short and the settings are reviewed like code. Here the client’s imports suit a bundler, and an overlay adjusts the document for the server:

version: '1.0'
commands:
  generate:
    client_fetch:
      import_extension: ''
    server_express:
      overlay:
        - overlays/express.overlay.yaml

Each target’s page lists its settings and the vendor extensions it reads, such as client fetch and server express. Extensions that matter to one generator only can live in an OpenAPI Overlay of that generator, listed under overlay or given with --overlay, instead of in the API description.

Regenerate in CI

Generate into the repository and let CI check that the generated code matches the contract, so nobody edits it by hand and nobody forgets to regenerate:

apigenie-generator client fetch openapi.yaml -o client --package-name shipping-client
apigenie-generator server express openapi.yaml -o server --package-name shipping-server
git diff --exit-code client server

Once the server runs, check that it keeps the contract with a validating proxy: Mock and contract-test an API.

Next steps