CRUD
ApiPlugin auto-generates REST endpoints for every schema. All query parameters are parsed server-side — no manual SQL, no query builders on the backend.
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /api/:schema | List records |
GET | /api/:schema/:id | Get a single record |
POST | /api/:schema | Create a record |
PATCH | /api/:schema/:id | Update a record |
DELETE | /api/:schema/:id | Delete a record |
Pagination
List endpoints are paginated by default.
GET /api/products?page=2&pageSize=10
Response:
{
"data": [...],
"meta": {
"page": 2,
"pageSize": 10,
"total": 84,
"totalPages": 9
}
}
Filtering
Filters use a nested bracket syntax.
GET /api/products?where[price][$gte]=100&where[isAvailable]=true
Operators
| Operator | Description |
|---|---|
$eq | Equals (default) |
$ne | Not equals |
$gt | Greater than |
$gte | Greater than or equal |
$lt | Less than |
$lte | Less than or equal |
$in | In array |
$nin | Not in array |
$like | Pattern match (SQL LIKE) |
$null | Is null / is not null |
Logical operators
GET /api/products?where[$or][0][price][$lt]=50&where[$or][1][stock][$gt]=100
Sorting
GET /api/products?sort=price // ascending
GET /api/products?sort=-price // descending
GET /api/products?sort=category,-price // multiple fields
Field selection
Return only specific fields:
GET /api/products?fields=name,price,stock
GET /api/products?fields=* // all fields (default)
Populate
Load related records inline.
GET /api/products?populate=* // all relations, shallow
GET /api/products?populate[category]=true // specific relation
GET /api/products?populate[category][fields]=name // with field selection
Nested populate:
GET /api/posts?populate[author][populate][company]=true
queryToParams
Building query strings by hand is error-prone. Use queryToParams from @datrix/api to serialize a typed query object into a URL query string.
import { queryToParams } from "@datrix/api";
const qs = queryToParams({
where: {
price: { $gte: 100 },
isAvailable: true,
},
populate: { category: true },
orderBy: ["-price"],
page: 2,
pageSize: 10,
} satisfies ParsedQuery<Product>);
const response = await fetch(`/api/products?${qs}`);
queryToParams is fully typed — it accepts the same ParsedQuery<T> shape that the server parses, so your client and server query shapes stay in sync.
Responses
Success — single record
{
"data": {
"id": 1,
"name": "Widget",
"price": 49.99
}
}
Success — list
{
"data": [
{
"id": 1,
"name": "Widget"
}
],
"meta": {
"page": 1,
"pageSize": 25,
"total": 1,
"totalPages": 1
}
}
Error
{
"error": {
"type": "DatrixApiError",
"code": "RECORD_NOT_FOUND",
"message": "product with id 99 not found",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}