---
title: "End-to-End TypeScript API Contracts: From Backend Schema to Frontend Usage"
date: 2025-05-26
draft: false
---

[⬇️ **Download this article as Markdown**](./05-26-2025-typescript-contracts-end-to-end.md)

> **Background:** This article builds on the foundation described in [Automatic TypeScript Generation from Backend Schema](./05-18-2025-automatic-typescript-generation.md). If you haven't read that, start there for the rationale and initial setup.

# End-to-End TypeScript API Contracts: From Backend Schema to Frontend Usage

## Overview

This article documents the complete, automated workflow that keeps our frontend TypeScript types in perfect sync with our backend API contract. We cover every step: from backend schema exposure, through Makefile automation and type generation, to ergonomic, type-safe API usage in React components.

---

## 1. Backend: Exposing the OpenAPI Schema

Our Go backend exposes an OpenAPI schema at `/` (or `/openapi.json`). This schema is generated automatically from registered routes and Go types, ensuring all request/response contracts are captured. See the [previous article](./05-18-2025-automatic-typescript-generation.md) for details on how pointers, time.Time, and required/optional fields are handled.

---

## 2. Makefile Automation: Generating Types

The Makefile orchestrates the entire type generation process:

```makefile
run-backend: ensure-db ensure-mock-ai
	# ...
	@curl -s http://localhost:8080/ > schema.json

schema.json: run-backend

generate-types: schema.json
	@mkdir -p web-app/src/types
	@npx openapi-typescript schema.json --output web-app/src/types/backend.d.ts
	@cd web-app && node scripts/generate-api-contracts.js
```

- The backend is started and the OpenAPI schema is fetched to `schema.json`.
- `openapi-typescript` generates `web-app/src/types/backend.d.ts` from the schema.
- A custom Node.js script generates API contract types in `web-app/src/types/index.d.ts`.

---

## 3. TypeScript Type Generation

`openapi-typescript` produces a `components` namespace in `backend.d.ts`:

```typescript
// web-app/src/types/backend.d.ts
export namespace components {
  export namespace schemas {
    export interface MetricResponse { /* ... */ }
    export interface MetricInput { /* ... */ }
    // ...
  }
}
```

---

## 4. API Contract Type Generation

The script `web-app/scripts/generate-api-contracts.js` reads the OpenAPI schema and emits strongly-typed API contract types for every route:

```typescript
// web-app/src/types/index.d.ts
import type { components } from './backend';
import type { APIContract } from './contract';

export type MetricsGetAPI = APIContract<never, components['schemas']['MetricResponse'][], '/metrics', 'get'>;
export type MetricsPutAPI = APIContract<components['schemas']['MetricUpdateInput'], components['schemas']['MetricResponse'], '/metrics', 'put'>;
// ...
```

Each type encodes the input, output, path, and method for a specific API route.

---

## 5. The `APIContract` Type

All API contract types are based on a generic utility:

```typescript
// web-app/src/types/contract.d.ts
export type APIContract<Input, Output, Path extends string, Method extends string = 'get' | 'post' | 'put'> = {
  Input: Input;
  Output: Output;
  path: Path;
  method: Method;
};
```

This enables precise typing for every endpoint.

---

## 6. The `useApiContract` Hook

Our custom React hook provides a type-safe, ergonomic way to call any API route using its contract type:

```typescript
// web-app/src/App/hooks/useApiContract.ts
export function useApiContract<Contract extends APIContract<any, any, string, 'get'>>(
  args: { path: Contract['path']; method: 'get' } & UseAsyncResourceOptions
): { response: AsyncResourceResponse<Contract['Output']>; call: () => void };

export function useApiContract<Contract extends APIContract<any, any, string, 'post'>>(
  args: { path: Contract['path']; method: 'post' } & UseAsyncResourceOptions
): { response: AsyncResourceResponse<Contract['Output']>; call: (input: Contract['Input']) => void };

// ...implementation...
```

The hook enforces correct input/output types for each method and path.

---

## 7. Real Usage: MetricsPage Example

In a real component, usage is simple and fully type-safe:

```typescript
// web-app/src/App/MetricsPage/index.tsx
const allMetrics = useApiContract<MetricsGetAPI>({
  path: '/metrics',
  method: 'get',
  immediate: true,
});
const updateMetric = useApiContract<MetricsPutAPI>({
  path: '/metrics',
  method: 'put',
});
```

- `allMetrics.response` is always of type `AsyncResourceResponse<MetricsGetAPI['Output'][]>`.
- `updateMetric.call` requires a `MetricsPutAPI['Input']` and starts the async logic for returning a `MetricsPutAPI['Output']` through `updateMetric.response.data`, which can be matched on via `match(updateMetric.response).with(...)`.

---

## 8. Benefits: Safe, Type-Driven API Usage

- **No manual sync:** Types are always up-to-date with the backend.
- **Type errors are caught at compile time.**
- **Ergonomic developer experience:** API usage is as simple as calling a hook with the right contract type.
- **Automatic contract validation:** The dev workflow fails fast if there is a mismatch.

---

## 9. References

- [Automatic TypeScript Generation from Backend Schema](./05-18-2025-automatic-typescript-generation.md)
- [openapi-typescript](https://github.com/drwpow/openapi-typescript)
- [TypeScript](https://www.typescriptlang.org/)
- [ts-pattern](https://github.com/gvergnaud/ts-pattern)

---

[⬇️ **Download this article as Markdown**](./05-26-2025-typescript-contracts-end-to-end.md) 