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

MethodPathDescription
GET/api/:schemaList records
GET/api/:schema/:idGet a single record
POST/api/:schemaCreate a record
PATCH/api/:schema/:idUpdate a record
DELETE/api/:schema/:idDelete 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

OperatorDescription
$eqEquals (default)
$neNot equals
$gtGreater than
$gteGreater than or equal
$ltLess than
$lteLess than or equal
$inIn array
$ninNot in array
$likePattern match (SQL LIKE)
$nullIs 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"
  }
}