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 for the signed-in user, exactly like ctx.db.asUser().
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,titlesorts; ascending unless.desc.limitis 50 by default, at most 100.offsetskips rows.nextOffsetis where the next page starts, ornullon 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 untilnextOffsetisnull.
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. |
As Markdown: apps/data-api.md