# TypeScript

Source: https://capacms.com/docs/guides/typescript

Generate types from your models with capa-codegen, then get reads typed from exactly what they select.

`@capacms/sdk` ships two commands. `capa-codegen` writes TypeScript types from your project's models. `capa-codegen --graphql` also types every GraphQL document in your code. After that, a read is typed from what it selects, and a misspelled field fails to compile.

The examples use an `articles` model with `title`, `body`, `views`, `featured`, `tags`, `author` and `coauthors`, and an `authors` model with `name` and `bio`.

## Install

```bash
pnpm add @capacms/sdk@next
pnpm add -D graphql
```

The SDK runs on Node 18 or later. Its types need TypeScript 5.0 or later. `graphql` is only needed by the codegen.

Set the two variables every Capa tool reads, in your shell or in `.env.local`:

```bash
CAPA_API_URL=https://cdn.capacms.com
CAPA_KEY=cap_live_...
```

`capa-codegen` reads them from the shell first, then from `.env.local` and `.env`, as `next dev` does.

## Generate model types

```bash
pnpm capa-codegen --out src/capa-types.ts
```

You get one interface per model the key can read, and a `<Model>Select` type beside each:

```ts
export interface Articles {
  title?: string;
  body?: string;
  views?: number;
  featured?: boolean;
  tags?: string[];
  author?: CapaRelation<Authors>;
  coauthors?: CapaRelationList<Authors>;
}

export type ArticlesSelect = import("@capacms/sdk/next").Select<Articles>;
```

The interfaces are read from the key's schema, so a key limited to one model writes one interface. Every field is optional, and an options field is typed `string`.

**Commit the file.** Generating at install time needs credentials in CI and Docker builds. A committed file turns "a model changed" into a reviewable diff.

## Type a REST read

Pass the model interface and the select's own type. `fields` is then typed exactly as the API returns it:

```ts
import { createClient } from "@capacms/sdk/next";
import type { Articles, ArticlesSelect } from "./capa-types";

const capa = createClient({
  baseUrl: process.env.CAPA_API_URL!,
  apiKey: process.env.CAPA_KEY!,
  version: "2026-10-01",
});

const select = [
  "title",
  "views",
  { author: ["name"] },
  { coauthors: { select: ["name"], limit: 3 } },
] as const satisfies ArticlesSelect;

const popular = await capa.entries.list<Articles, typeof select>("articles", {
  select,
  sort: ["-views"],
  limit: 10,
});

for (const article of popular.data) {
  const { title, views, author, coauthors } = article.fields;
  if (author && "fields" in author) console.log(title, views, author.fields.name);
  for (const coauthor of coauthors.items) {
    if ("fields" in coauthor) console.log("  with", coauthor.fields.name);
  }
}
```

What the types say:

* A field the select names is always present, and `null` when it was never filled.
* A field the select does not name is not there. Reading `article.fields.body` above does not compile.
* An expanded relation is the related entry with its own `fields`, or `{ id, model, missing: true }` when that entry is gone or unpublished. `"fields" in author` tells them apart.
* A relation you name without expanding is a reference, `{ id, model }`.
* A relation list is `{ items, pageInfo }`.

A misspelled field in `select` fails to compile, because of `satisfies ArticlesSelect`.

## Type GraphQL documents

Write a document where you use it, marked as `#graphql`:

```ts
import { createClient } from "@capacms/sdk/next";

const capa = createClient({
  baseUrl: process.env.CAPA_API_URL!,
  apiKey: process.env.CAPA_KEY!,
  version: "2026-10-01",
});

const LATEST = `#graphql
  query Latest($first: Int) {
    articles(first: $first, sort: [publishedAt_DESC]) {
      nodes { id title author { name } }
    }
  }
`;

const { data, errors } = await capa.graphql(LATEST, { first: 5 });
for (const error of errors) console.warn(error.code, error.message);
for (const article of data?.articles?.nodes ?? []) {
  console.log(article.title, article.author?.name);
}
```

Then generate the module that types it:

```bash
pnpm capa-codegen --graphql --out src/capa-graphql.ts
```

Now `data` and the variables are typed from the document itself. `capa.graphql(LATEST, { first: "5" })` fails to compile.

A `#graphql` literal that codegen has not read yet does not compile either. The error says to run `capa-codegen --graphql`, so a document you edited is never silently untyped.

Keep both commands in `package.json`:

```json
{
  "scripts": {
    "dev": "capa-codegen --graphql --watch --out src/capa-graphql.ts & next dev",
    "prebuild": "capa-codegen --graphql --out src/capa-graphql.ts"
  }
}
```

`--watch` rewrites the module when a document changes, and when the key's schema does. It checks the schema once a minute.

Codegen also reads `.graphql` files and `gql` tagged templates under `src`. For those, import the `<Name>Document` it writes for every named operation.

## Build queries as objects

Pass `CapaQuery` from the generated module and write the query as an object. The result is typed from exactly what you select:

```ts
import { createClient } from "@capacms/sdk/next";
import type { CapaQuery } from "./capa-graphql";

const capa = createClient<CapaQuery>({
  baseUrl: process.env.CAPA_API_URL!,
  apiKey: process.env.CAPA_KEY!,
  version: "2026-10-01",
});

const { data } = await capa.graphql.query({
  articles: {
    args: { first: 5, sort: ["publishedAt_DESC"], filter: { featured: { eq: true } } },
    nodes: { id: true, title: true, author: { name: true } },
  },
});

console.log(data?.articles?.nodes[0]?.author?.name);
```

`true` selects a field. An object selects inside a relation. `args` sits beside the fields of anything that takes arguments. A misspelled field, filter or sort value fails to compile, and the error names it.

## Type a component's props

`NodeOf` names one entry of a builder read. A component that takes it cannot drift from the query:

```tsx
import type { NodeOf } from "@capacms/sdk/next";
import type { CapaQuery } from "./capa-graphql";

const teasers = {
  articles: { args: { first: 5 }, nodes: { id: true, title: true, author: { name: true } } },
} as const;

type Teaser = NodeOf<CapaQuery, typeof teasers, "articles">;

export function TeaserCard({ article }: { article: Teaser }) {
  return (
    <li>
      {article.title} by {article.author?.name}
    </li>
  );
}
```

## In CI

Fail the build when the committed types are stale:

```bash
pnpm capa-codegen --graphql --check --out src/capa-graphql.ts
```

| Exit code | Meaning                                                         |
| --------- | --------------------------------------------------------------- |
| `0`       | Written, or already current.                                    |
| `1`       | Failed: a document has a problem, or the API could not be read. |
| `2`       | `--check` found the committed file out of date.                 |

No key in CI? Save the schema where you have one, commit it, and read it back:

```bash
pnpm capa-codegen --graphql --save-schema capa-schema.json --out src/capa-graphql.ts
pnpm capa-codegen --graphql --schema capa-schema.json --check --out src/capa-graphql.ts
```

A field the key cannot read is a codegen error with its file and line, so a model change fails CI instead of production.

## Related

* [SDK reference](https://capacms.com/docs/sdk) for every client option.
* [SDK CLI](https://capacms.com/docs/sdk/cli) for every flag of `capa-codegen` and `capa persist`.
* [GraphQL clients](https://capacms.com/docs/guides/graphql-clients) for Apollo, urql and persisted queries.
