# Content API

> Serve your content over HTTP to frontends that cannot open the index themselves, with the same typed reads on the other end.

*Serve your content over HTTP, for frontends that cannot read the index directly.*

Usually your app reads content straight from the index: a file on the static
engine, or the database on Postgres. Sometimes it cannot. A browser app has
neither, and a frontend deployed separately from your content may not either.

The Content API covers that case. A small HTTP handler serves your content,
and a matching reader on the frontend fetches it. You give the reader to any
Graft SDK as its `index`, and your code reads content exactly as before:
`getContent`, `listContent`, `searchContent`, fully typed.

It is read-only and serves one branch. This page shows how to serve it, read
from it, and keep it private, then lists the endpoints in full.

## Serve it

`graft serve` already serves it at `/api/content/v1`. To serve it from your own
app, create the handler with the index reader you already have:

`app/api/content/v1/[...path]/route.ts`:

```ts
import { createContentApiHandler } from "@usegraft/content-api";
import { createDbIndexReader } from "@usegraft/db";

const handler = createContentApiHandler({
  collections: ["docs", "pages"], // only these are served
  branch: "main",
  index: createDbIndexReader(db),
});

export const GET = (request: Request) => handler(request);
export const OPTIONS = GET; // browser preflight, when allowedOrigins is set
```

`collections` is the list of what is published. A collection not on it does
not exist as far as the endpoint is concerned. `graft serve` publishes every
collection in your config, so serve the handler yourself if you need a shorter
list.

## Read from it

On the frontend, create a reader for the endpoint and pass it to your SDK as
the `index`:

`src/lib/graft.ts`:

```ts
import { createContentApiReader } from "@usegraft/content-api";
import { createGraft } from "@usegraft/sdk-astro";
import { collections } from "../../graft.config";

export const graft = createGraft({
  index: createContentApiReader({
    endpoint: "https://cms.example.com/api/content/v1",
    headers: { authorization: `Bearer ${process.env.GRAFT_CONTENT_TOKEN}` },
  }),
  collections,
});
```

| Option | What it does |
| --- | --- |
| `endpoint` | The API's base URL |
| `headers` | Headers sent with every request, such as `Authorization` |
| `fetch` | Your own `fetch`, for tests, instrumentation, or runtimes without one |

Every document is checked against your schema when it arrives, just as a local
read is, so a document that does not match never reaches your code.

Errors keep their meaning over HTTP. A Graft error on the server arrives as the
same Graft error on the client, with its `code` and `fix`. Anything else, such
as a proxy returning HTML, becomes `FUNCTION_EXECUTION_FAILED` with a message
saying what went wrong.

## It does not authenticate requests

The handler checks no identity. Every document in every published collection is
readable by anyone who can reach the URL. That is deliberate: published content
is usually public, and any login scheme the handler guessed at would be wrong
for most sites.

So the `collections` list is your security boundary. If some content must stay
private, do not publish its collection here. If the whole endpoint must be
private, put a proxy in front of it that checks a token, and send that token
with the reader's `headers`. Do not rely on nobody finding the URL.

The one protection built in is a [rate limit](/docs/rate-limits#the-content-api).
`graft serve` sets 60 requests per 60 seconds. A handler you mount yourself has
none unless you pass `rateLimit`.

## Read it from a browser

A browser blocks reads from another origin unless the server allows them, and
the handler allows none by default. If your browser app is served from a
different origin than the API, list the origins that may read it:

```ts
createContentApiHandler({
  collections,
  branch: "main",
  index,
  allowedOrigins: ["https://app.example.com"],
});
```

On `graft serve`, set `GRAFT_CONTENT_ALLOWED_ORIGINS` to a comma-separated list,
or to `*`. Allowing every origin gives nothing away here, since anyone can
already read the endpoint from outside a browser.

Error responses carry the same headers, so a failing browser request shows its
`fix` instead of an opaque network error.

## Endpoints

Two routes, both `GET`:

| Route | Required | Optional | Returns |
| --- | --- | --- | --- |
| `/api/content/v1/documents` | `collection` | `slug`, `limit`, `offset` | `{ "rows": [...] }` |
| `/api/content/v1/search` | `collection`, `query` | `limit` | `{ "hits": [{ row, rank, snippet }] }` |

```sh
curl "https://cms.example.com/api/content/v1/documents?collection=docs&slug=tiers"
curl "https://cms.example.com/api/content/v1/search?collection=docs&query=branching"
```

`limit` must be a positive whole number, and values above 500 are treated as
500. `offset` must be zero or more. Search takes one collection at a time. The
`query` syntax is the same as [Search](/docs/search).

### Each row

| Field | Type |
| --- | --- |
| `collection`, `slug` | string |
| `data` | object, the checked frontmatter |
| `body` | string, the Markdown or MDX body |
| `sourcePath` | string, the file to edit |
| `contentHash` | string |
| `branchId` | string |
| `deleted` | boolean |
| `updatedAt` | string, ISO 8601 |
| `search` | string or null |

### One endpoint serves one branch

A `branch` query parameter is refused, so no request can switch a production
endpoint onto preview content. For preview reads, serve a second endpoint for
that branch.

### Errors

| Status | Code | When |
| --- | --- | --- |
| 400 | `INPUT_VALIDATION_FAILED` | A bad `limit` or `offset`, an empty `slug`, a missing `collection` or `query`, or a `branch` parameter |
| 404 | `COLLECTION_NOT_FOUND` | The collection is not published on this endpoint. The `fix` lists those that are. |
| 404 | `ROUTE_NOT_FOUND` | Nothing is served at that path |
| 405 | `METHOD_NOT_ALLOWED` | Anything other than `GET` |
| 500 | `FUNCTION_EXECUTION_FAILED` | The endpoint could not read its index |

## Limitations

- **Read-only.** Content is written through files, the CLI, or MCP.
- **One branch per endpoint.**
- **No operational data.** User data is only reachable through
  [typed functions](/docs/functions-and-access).
- **No authentication.** See above.

## Next steps

- [Reading content](/docs/reading-content) covers the reads this serves.
- [Deploy](/docs/deploy) shows where the endpoint runs.
- [React](/docs/react) reads it from the browser.
