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
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. Returns 404 if not found.
curl http://localhost:3000/todos/one/42Response:
{ "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.
Composite keys — use query parameters instead of a path parameter:
curl "http://localhost:3000/task-tags/one?taskId=1&tagId=2"The controller matches query parameters against composite primary keys first, then compound unique indexes.
GET /meta
Returns table or view metadata for use by UI tooling or client libraries.
curl http://localhost:3000/todos/metaResponse:
{
"searchable": true,
"vectorSearchable": false,
"searchIndexes": [
{ "name": "DEFAULT", "description": "dynamic_text index" }
],
"type": { "...": "serialized Atscript type definition" }
}| 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 |
type | Full serialized Atscript type (field names, types, annotations, metadata) |
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 }See Update & Patch for details on patch semantics and embedded object updates.
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)