CRUD Endpoints
This page documents every endpoint generated by AsDbController and AsDbReadableController. All paths are relative to the controller prefix (e.g., /todos/).
See HTTP Setup for installation and wiring.
Reading Records
Read-response baseline
Every row-returning read endpoint (/query, /pages, /one, /one/:id, including $search and vector-search paths) silently widens the projection so each row carries the table's preferredId field set, regardless of $select.
$select shape | Behaviour |
|---|---|
absent / undefined | full projection — preferred-id fields already present |
string[] inclusion | dedupe + append missing preferred-id fields |
pure inclusion map ({ name: 1 }) | add missing preferred-id keys with value 1 |
pure exclusion map ({ id: 0 }) | rewritten to inclusion (all non-ignored own-table fields minus excluded) + every preferred-id field — exclusion CANNOT remove a preferred-id field |
mixed inclusion/exclusion ({ a: 1, b: 0 }) | rejected before the readable call (HTTP 400) |
Not widened: $groupBy aggregate (group keys are the only fields) and $count (returns a number).
preferredId defaults to primaryKeys. Declare @db.table.preferredId.uniqueIndex(name?) to use a specific @db.index.unique group as the preferred identifier — see Actions — Preferred row identifier. The widening happens AFTER any transformProjection() override resolves; preferred-id fields cannot be suppressed via projection. Hide identifiers at the network/authz layer instead.
GET /query
Returns an array of records. Supports filtering, sorting, pagination, projection, relation loading, and search.
curl "http://localhost:3000/todos/query?completed=false&\$sort=-createdAt&\$limit=10"Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
$sort | string | — | Sort expression (e.g., -createdAt for descending) |
$limit | number | 1000 | Maximum records to return |
$skip | number | 0 | Number of records to skip |
$select | string | — | Comma-separated field names for projection |
$count | boolean | — | Return count instead of records |
$search | string | — | Fulltext search term |
$index | string | — | Named search index |
$vector | string | — | Vector field name for similarity search |
$threshold | string | — | Similarity threshold for vector search |
$with | string | — | Load relations (e.g., $with=author,comments) |
$groupBy | string | — | Group by fields for aggregation |
| (other) | any | — | Filter fields (e.g., status=active) |
Response (array):
[
{ "id": 1, "title": "Buy milk", "completed": false, "priority": "high" },
{ "id": 2, "title": "Write docs", "completed": false, "priority": "medium" }
]When $count is set, returns a number instead of an array:
curl "http://localhost:3000/todos/query?completed=true&\$count"5See URL Query Syntax for the full filter syntax and Relations & Search for $with, $search, $vector, and $groupBy.
GET /pages
Returns paginated results with metadata. Uses page-based pagination instead of offset-based.
curl "http://localhost:3000/todos/pages?\$page=2&\$size=10&status=active"Additional parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
$page | number | 1 | Page number (1-based) |
$size | number | 10 | Items per page |
All other query parameters from GET /query (filters, $sort, $select, $search, $with) are also supported, except $skip, $limit, and $count.
Response:
{
"data": [{ "id": 11, "title": "Task 11", "status": "active" }],
"page": 2,
"itemsPerPage": 10,
"pages": 5,
"count": 47
}| Field | Description |
|---|---|
data | Array of records for the current page |
page | Current page number |
itemsPerPage | Page size |
pages | Total number of pages |
count | Total number of matching records |
GET /one/:id
Retrieves a single record by primary key (or any single-field unique index — the path resolver walks every legitimate identification). Returns 404 if not found.
curl http://localhost:3000/todos/one/42
curl http://localhost:3000/users/one/admin # by username unique indexResponse:
{ "id": 42, "title": "Buy milk", "completed": false, "priority": "high" }Supports $select and $with in the query string:
curl "http://localhost:3000/todos/one/42?\$select=id,title&\$with=project"No filters
Filter parameters (like status=active) are not allowed on this endpoint. They return a 400 error. Use GET /query with filters instead.
When the table declares @db.table.preferredId.uniqueIndex (the canonical addressing identifier), scalar lookups via /one/:id resolve against the preferredId field first; PK and other unique indexes are not retried for the same scalar (the named form below covers them when needed).
Named-form (object) addressing — use query parameters when you want to be explicit about which identification you are addressing, or when the identifier is compound:
curl "http://localhost:3000/users/one?username=admin" # single-field unique index
curl "http://localhost:3000/task-tags/one?taskId=1&tagId=2" # composite primary keyThe controller walks every registered identification (primary key + every unique index, single-field and compound) in declaration order and picks the first whose fields are all present in the query.
GET /meta
Returns table or view metadata for use by UI tooling or client libraries.
curl http://localhost:3000/todos/metaResponse (abbreviated):
{
"searchable": true,
"vectorSearchable": false,
"searchIndexes": [{ "name": "DEFAULT", "description": "dynamic_text index" }],
"primaryKeys": ["id"],
"preferredId": ["id"],
"relations": [{ "name": "comments", "direction": "from", "isArray": true }],
"fields": {
"id": { "sortable": true, "filterable": true },
"title": { "sortable": true, "filterable": true }
},
"type": { "...": "serialized Atscript type definition" },
"actions": [],
"crud": {
"query": ["filter", "insights", "...", "actions", "groupBy"],
"pages": ["filter", "page", "size", "...", "actions"],
"one": ["select", "with", "actions"],
"insert": [],
"update": [],
"replace": [],
"remove": []
}
}| Field | Description |
|---|---|
searchable | Whether the table has fulltext search indexes |
vectorSearchable | Whether the table has vector search indexes |
searchIndexes | Array of available search index definitions |
primaryKeys | Primary key field names (logical, not column names) |
preferredId | Logical field names of the preferred identifier (PK or @db.table.preferredId.uniqueIndex group). See Preferred row identifier |
relations | Available navigation properties |
fields | Per-field capability flags (sortable, filterable) |
type | Full serialized Atscript type (field names, types, annotations, metadata). FK fields ship as shallow refs ({ id, metadata }) — enough to resolve the target's URL via db.http.path; deeper structure is reachable through the target's own /meta (see below) |
actions | Declared domain actions — see Actions |
crud | Built-in CRUD permissions / control whitelists — see Permissions |
For the full payload shape including actions[] entries and complete crud whitelists, see HTTP Client — Metadata.
The type.metadata["db.http.path"] carried in this payload follows the normalization contract — it is always the final public URL, prefixed with / and inclusive of the Moost globalPrefix, safe to use verbatim with fetch() or new Client(url).
FK ref shape in meta
/meta always serializes FK fields as shallow refs — ref.type is the { id, metadata } shape, independent of @db.depth.limit (which is a security guard on nested writes, not a serialization policy). The target's db.http.path is carried in metadata, so clients can:
- resolve the target endpoint for value-help pickers, and
- fetch the target's own
/metaon demand when they need the target's structural body.
Nav-prop trees (@db.rel.from / @db.rel.to / @db.rel.via) are fully expanded in meta regardless — they are not .ref nodes — so the write-payload shape clients need for nested inserts is always present. Only the FK pointer bodies are shallow, and those are recoverable via a cached per-target /meta fetch.
Creating Records
POST /
Insert one or many records. The request body determines single vs. batch mode.
Single insert:
curl -X POST http://localhost:3000/todos/ \
-H "Content-Type: application/json" \
-d '{"title": "Buy milk", "priority": "high"}'Response:
{ "insertedId": 1 }Batch insert:
curl -X POST http://localhost:3000/todos/ \
-H "Content-Type: application/json" \
-d '[{"title": "Buy milk"}, {"title": "Write docs"}]'Response:
{ "insertedCount": 2, "insertedIds": [1, 2] }Default values from @db.default and generated defaults (@db.default.increment, @db.default.uuid, @db.default.now) are applied automatically. Request bodies can contain nested relation data for deep insert operations.
Batch edge cases
- Empty array
[]— behavior is adapter-dependent (may return 200, 201, 400, or 500) - Single-item array
[{...}]— treated as a batch insert, returnsinsertedCount/insertedIds - Large batches (100+ items) — supported; the entire batch runs in a single transaction
- Partial failure — if any item fails validation or violates a constraint, the entire batch is rolled back
Updating Records
PUT /
Full replace by primary key. The body must include all required fields and the primary key field(s).
Single replace:
curl -X PUT http://localhost:3000/todos/ \
-H "Content-Type: application/json" \
-d '{"id": 1, "title": "Buy oat milk", "completed": true, "priority": "high"}'Response:
{ "matchedCount": 1, "modifiedCount": 1 }Bulk replace:
curl -X PUT http://localhost:3000/todos/ \
-H "Content-Type: application/json" \
-d '[
{"id": 1, "title": "Buy oat milk", "completed": true, "priority": "high"},
{"id": 2, "title": "Write tests", "completed": false, "priority": "medium"}
]'Response:
{ "matchedCount": 2, "modifiedCount": 2 }Nested relation data is supported per item — each record goes through the deep replace process.
PATCH /
Partial update by primary key. Only the provided fields are changed.
Single update:
curl -X PATCH http://localhost:3000/todos/ \
-H "Content-Type: application/json" \
-d '{"id": 1, "completed": true}'Response:
{ "matchedCount": 1, "modifiedCount": 1 }Bulk update:
curl -X PATCH http://localhost:3000/todos/ \
-H "Content-Type: application/json" \
-d '[{"id": 1, "completed": true}, {"id": 2, "priority": "high"}]'Response:
{ "matchedCount": 2, "modifiedCount": 2 }Field operations ($inc, $dec, $mul) work as plain JSON objects in the body:
curl -X PATCH http://localhost:3000/products/ \
-H "Content-Type: application/json" \
-d '{"id": 42, "views": {"$inc": 1}, "stock": {"$dec": 1}}'Also supports array patch operators. See Update & Patch for the full programmatic API.
Deleting Records
DELETE /:id
Removes a single record by primary key. Returns 404 if the record is not found.
curl -X DELETE http://localhost:3000/todos/42Response:
{ "deletedCount": 1 }Composite keys — use query parameters:
curl -X DELETE "http://localhost:3000/task-tags/?taskId=1&tagId=2"Response:
{ "deletedCount": 1 }Error Handling
The controller automatically transforms errors into appropriate HTTP responses:
| Error | HTTP Status | Response Body |
|---|---|---|
ValidatorError | 400 | { message, statusCode, errors: [{ path, message }] } |
DbError (CONFLICT) | 409 | { message, statusCode, errors } |
DbError (other) | 400 | { message, statusCode, errors } |
| Not found | 404 | Standard 404 |
Validation error example:
{
"message": "Validation failed",
"statusCode": 400,
"errors": [
{ "path": "title", "message": "Required field" },
{ "path": "project.title", "message": "Expected string, got number" },
{ "path": "tasks.0.status", "message": "Required field" }
]
}Validation runs automatically on POST, PUT, and PATCH using the constraints defined in your .as schema.
Query Validation
Invalid query parameters return 400 errors with descriptive messages:
| Invalid query | Error reason |
|---|---|
$with=nonexistent | Navigation property does not exist |
$with=projectId | FK field, not a navigation property |
$with=tasks($with=nonexistent) | Nested relation does not exist |
$select=fakefield | Field does not exist on the type |
$sort=nonexistent | Cannot sort by unknown field |
GET /one/1?status=todo | Filters not allowed on getOne endpoint |
These validations apply to all endpoints that accept query controls — /query, /pages, and /one/:id.
Next Steps
- URL Query Syntax — Full filter, sort, and pagination syntax
- Relations & Search in URLs —
$with,$search,$vector,$groupByin query strings - Customization — Override hooks for access control and data transformation
- CRUD Operations — Programmatic
AtscriptDbTableAPI (non-HTTP) - HTTP Client — TypeScript client for consuming these endpoints