Skip to content

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,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.

As Markdown: apps/data-api.md