Records & Dynamic CRUD
Create, read, update, filter, paginate, sort, and delete dynamic collection records via REST API.
Moul provides instant, fully typed REST API endpoints for every dynamic collection. Record operations are validated against the schema and checked against the collection's access rules.
Query Parameters
List queries (GET /api/moul/:name/records) accept the following standard query parameters:
| Parameter | Type | Default | Description | Example |
|---|---|---|---|---|
page | integer | 1 | 1-indexed page number. | page=2 |
perPage | integer | 30 | Number of records per page (max 500). | perPage=50 |
sort | string | -created | Comma-separated sort fields. Prefix with - for descending. | sort=-views,title |
filter | string | "" | Filter expression evaluated against SQLite records. | filter=published=true && views>100 |
expand | string | "" | Comma-separated relation field names to expand inline. | expand=authorId,categories |
fields | string | * | Comma-separated fields to return in the payload. | fields=id,title,createdAt |
reveal | string | "" | Comma-separated cloak field names (or all / *) to unmask decrypted plaintext for authenticated users. | reveal=ssn,apiKey |
Standard List Response Format
{
"page": 1,
"perPage": 30,
"totalItems": 142,
"totalPages": 5,
"items": [
{
"id": "rec_01J6XYZ123",
"title": "Getting Started with Moul",
"slug": "getting-started-with-moul",
"published": true,
"views": 420,
"createdAt": "2026-08-01T12:00:00Z",
"updatedAt": "2026-08-02T15:30:00Z"
}
]
}CRUD Operations
1. List Records (GET /api/moul/:name/records)
curl -s "http://localhost:8090/api/moul/posts/records?page=1&perPage=20&sort=-created&filter=published=true"const params = new URLSearchParams({
page: '1',
perPage: '20',
sort: '-created',
filter: 'published = true',
});
const res = await fetch(`http://localhost:8090/api/moul/posts/records?${params}`);
const data = await res.json();
console.log(`Found ${data.totalItems} posts:`, data.items);req, _ := http.NewRequest("GET", "http://localhost:8090/api/moul/posts/records?page=1&perPage=20", nil)
resp, err := http.DefaultClient.Do(req)
// parse JSON...2. View Single Record (GET /api/moul/:name/records/:id)
curl -s "http://localhost:8090/api/moul/posts/records/rec_01J6XYZ123"3. Create Record (POST /api/moul/:name/records)
curl -X POST "http://localhost:8090/api/moul/posts/records" \
-H "Authorization: Bearer <user_token>" \
-H "Content-Type: application/json" \
-d '{
"title": "Deploying on Bare Metal",
"slug": "deploying-on-bare-metal",
"content": "A complete walk-through...",
"published": true
}'const res = await fetch('http://localhost:8090/api/moul/posts/records', {
method: 'POST',
headers: {
'Authorization': `Bearer ${userToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: 'Deploying on Bare Metal',
slug: 'deploying-on-bare-metal',
content: 'A complete walk-through...',
published: true,
}),
});
const record = await res.json();payload := map[string]any{
"title": "Deploying on Bare Metal",
"slug": "deploying-on-bare-metal",
"published": true,
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "http://localhost:8090/api/moul/posts/records", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+userToken)
req.Header.Set("Content-Type", "application/json")4. Update Record (PATCH /api/moul/:name/records/:id)
curl -X PATCH "http://localhost:8090/api/moul/posts/records/rec_01J6XYZ123" \
-H "Authorization: Bearer <user_token>" \
-H "Content-Type: application/json" \
-d '{"views": 500}'5. Delete Record (DELETE /api/moul/:name/records/:id)
curl -X DELETE "http://localhost:8090/api/moul/posts/records/rec_01J6XYZ123" \
-H "Authorization: Bearer <user_token>"
# Returns HTTP 204 No Content on successEncrypted Cloak Fields in Record CRUD
Collections with cloak fields allow storing sensitive data with end-to-end server encryption:
Creating & Updating Records
Submit plaintext strings in your JSON payload as normal. Moul automatically derives AES-256-GCM and HMAC keys, encrypts the value, and stores companion blind index hashes transparently:
curl -X POST "http://localhost:8090/api/moul/customers/records" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"name": "Alice Smith", "ssn": "123-45-6789"}'Reading & Revealing Plaintext
By default, record reads return masked strings (•••• + last 4 characters) to prevent accidental exposure in UI tables, logs, and client bundles:
{
"id": "rec_123",
"name": "Alice Smith",
"ssn": "••••6789"
}To unmask specific fields, append ?reveal=<fieldName> or ?reveal=all:
curl "http://localhost:8090/api/moul/customers/records/rec_123?reveal=ssn" \
-H "Authorization: Bearer <token>"Administrators (authenticated via master X-Admin-Key or _rootUsers token) always receive decrypted unmasked values automatically.
Exact Match Filtering via Blind Indexes
If searchable: true was configured on the schema, filter expressions can perform exact equality searches:
curl "http://localhost:8090/api/moul/customers/records?filter=ssn = '123-45-6789'" \
-H "Authorization: Bearer <token>"Substring searches (~, !~) and range operators (>, <, >=, <=) are strictly forbidden on cloak fields and will return HTTP 400 Bad Request. Companion hash columns (ssnHash) are internal database details and are never exposed in API payloads.