# Data API

Mark a table `expose: true` and your app gets a REST API for it at `/api/data/<table>`, with no code. Every request runs under the table's [access rules](data.md) for the signed-in user, exactly like `ctx.db.asUser()`.

```ts
export const todos = table(
  "todos",
  { id: text().primaryKey(), userId: owner(), title: text().notNull(), done: integer({ mode: "boolean" }).default(false) },
  { expose: true },
);
```

Tables are off by default. A table that isn't exposed, or doesn't exist, answers `404 not_found`.

A table nobody may read or write can't be exposed: one with no `owner()` column and no `access` block, or with both rules `deny`. In TypeScript that doesn't compile; in JavaScript, the deploy fails because your app refuses to start.

## Requests

`<table>` is the table's name as you gave it to `table()`. Column names are the keys you used in the definition.

| Request | What it does |
| --- | --- |
| `GET /api/data/<table>` | Lists rows the user may read. Filters, `order`, `limit` and `offset` below. Returns `{ rows, limit, offset, nextOffset }`. |
| `GET /api/data/<table>/<id>` | One row by primary key: `{ row }`, or `404`. |
| `POST /api/data/<table>` | Creates a row (`{ row }`) or, with an array, up to 100 rows (`{ rows }`). Returns `201`. For owner tables the owner column is set to the user. |
| `PATCH /api/data/<table>/<id>` | Changes some columns of one row: `{ row }`, or `404` if it doesn't exist or the user may not write it. The primary key can't change. |
| `PATCH /api/data/<table>?<filters>` | Changes every matching row the user may write: `{ rows }`. Needs at least one filter. |
| `DELETE /api/data/<table>/<id>` | Deletes one row: `204`, or `404`. |
| `DELETE /api/data/<table>?<filters>` | Deletes every matching row the user may write: `{ deleted }`. Needs at least one filter. |

## Filters

Write each filter as `column=op.value`. Several filters must all match.

| Operator | Example |
| --- | --- |
| `eq`, `neq` | `done=eq.false` |
| `gt`, `gte`, `lt`, `lte` | `price=gte.500` |
| `like` (text only, `%` matches anything) | `title=like.milk%25` |
| `in` (comma-separated) | `id=in.a,b,c` |
| `is` | `dueAt=is.null`, `dueAt=is.notnull` |

Values are read for the column's type: numbers, `true`/`false`, and dates as ISO 8601 or milliseconds.

## Sorting and pages

- `order=createdAt.desc,title` sorts; ascending unless `.desc`.
- `limit` is 50 by default, at most 100. `offset` skips rows.
- `nextOffset` is where the next page starts, or `null` on the last page. For a table with a function read rule, rows are filtered after the query, so a page can be short; keep going until `nextOffset` is `null`.

## Bodies

Send JSON with the table's columns. Unknown columns, wrong types, `null` for a required column and invalid dates are `400 validation_failed`, with the column named in `details`.

## Errors

| Status | Code | When |
| --- | --- | --- |
| 400 | `validation_failed` | A bad filter, sort, page or body. |
| 401 | `unauthenticated` | The rule needs a signed-in user and nobody is signed in. |
| 403 | `forbidden` | The rule refuses the write, for example naming someone else as the owner. |
| 404 | `not_found` | No such exposed table, or no such row the user may see. |
| 507 | `database_storage_full`, `d1_size_limit` | Your database storage is full, or this project's D1 reached 10 GB: writes stop, reads and deletes still work. |