CRUD Server
The CRUD REST API server is built via createServers(repos).crud(...) — a unified factory that exposes standard RESTful endpoints and advanced query APIs for all registered repositories.
Features:
- Auto-generated REST endpoints (
GET,POST,PUT,PATCH,DELETE) - Advanced query endpoint (
POST /:repoName/query) withwhere,orWhere,orWhereGroups, andincludes - Atomic batch endpoint (
POST /:repoName/batch) - Server-side total counting (
withTotal) powered by Firestore.count()aggregation - Automatic OpenAPI 3.1 spec (
/openapi.json) and Scalar documentation UI (/docs) - Zod schema validation for input payloads
- Hono middleware support (Auth, logging, CORS, rate-limiting)
Basic setup
import { onRequest } from "firebase-functions/v2/https";
import { createServers } from "@lpdjs/firestore-repo-service";
const servers = createServers(repos, {
onRequest,
httpsOptions: { invoker: "public" },
});
export const api = servers.crud({
basePath: "/api",
middlewares: [
async (c, next) => {
// Custom auth / middleware logic
await next();
},
],
repos: {
users: {
path: "users",
filterableFields: ["email", "status", "role"],
orderableFields: ["createdAt", "name"],
allowDelete: true,
},
posts: {
path: "posts",
allowedIncludes: ["userId"],
allowDelete: false,
},
},
openapi: {
title: "My CRUD API",
version: "1.0.0",
description: "RESTful API powered by Firestore Repo Service",
},
});Endpoints overview
| Method | Endpoint | Description |
|---|---|---|
GET | /:repoName | List documents (paginated, with query filters and withTotal) |
GET | /:repoName/:id | Get single document by ID |
POST | /:repoName | Create a new document (Zod validated) |
PUT | /:repoName/:id | Replace document |
PATCH | /:repoName/:id | Partial update document |
DELETE | /:repoName/:id | Delete document |
POST | /:repoName/query | Advanced query (AND, OR groups, includes, total count) |
POST | /:repoName/batch | Execute atomic batch write operations |
GET | /openapi.json | OpenAPI 3.1 JSON specification |
GET | /docs | Interactive Scalar API documentation |
Listing documents (GET /:repoName)
Query parameters:
pageSize: Number of items per page (default: 25, max: 100)cursor: Base64 cursor token for paginationdirection: Pagination direction (next|prev)orderBy: Field to sort byorderDir: Sort direction (asc|desc)select: Comma-separated list of fields to projectincludes: Comma-separated list of relations to populatewithTotal: Set totrueto request total matching document count- Filter params:
?field=value,?field__gt=10,?field__in=a,b,?field__containsAny=x,y
Example:GET /api/users?status=active&pageSize=10&withTotal=true
Response:
{
"success": true,
"data": {
"items": [ ... ],
"nextCursor": "eyJ...",
"prevCursor": null,
"hasNextPage": true,
"hasPrevPage": false,
"totalCount": 142,
"totalCountIsExact": true
},
"meta": {
"pageSize": 10,
"hasMore": true,
"totalCount": 142,
"totalCountIsExact": true
}
}Advanced query (POST /:repoName/query)
Send complex filter conditions including where (AND), orWhere (simple OR), and orWhereGroups (compound OR).
Request Body:
{
"where": [["status", "==", "active"]],
"orWhereGroups": [
[["role", "==", "admin"], ["age", ">=", 18]],
[["role", "==", "moderator"]]
],
"orderBy": [{ "field": "createdAt", "direction": "desc" }],
"pageSize": 10,
"withTotal": true
}Total Count Behavior (withTotal):
- Pure AND queries & split
inclauses: Uses server-side.count()aggregation.totalCountIsExactistrue. - OR queries (
orWhere,orWhereGroups): Executes parallel server-side.count()queries across OR branches. Fast and lightweight (1000 docs = 1 index read).totalCountIsExactisfalse(estimated due to potential branch overlap).
Configuration options (CrudServerOptions)
| Option | Type | Description |
|---|---|---|
basePath | string | Base path prefix (e.g., "/api/v1") |
middlewares | MiddlewareHandler[] | Custom Hono middleware array |
repos | Record<string, CrudRepoConfig> | Per-repository configuration mapping |
indexesError | (err: { repoName, error, indexUrl, c }) => void | Callback triggered when a Firestore missing-index error occurs |
openapi | OpenAPISpecOptions | OpenAPI 3.1 documentation settings |
verbose | boolean | Include detailed error messages in HTTP 500 responses |
Per-Repository Configuration (CrudRepoConfig)
| Option | Type | Description |
|---|---|---|
path | string | Collection path |
filterableFields | string[] | List of allowed fields for filtering |
orderableFields | string[] | List of allowed fields for ordering |
allowedIncludes | string[] | List of relation keys allowed in includes |
allowDelete | boolean | Whether DELETE /:id is enabled |
rules | CrudRule[] | Before rules & diff engine for mutation validation |
pageSize | number | Default page size for list requests |
Before Rules & Diff Engine (rules)
You can define validation rules executed before document mutation (PUT, PATCH, and POST /:repoName/batch). The rule receives the existing document state (before), the simulated document state (after), the dictionary of modified fields (changes), operation type (op), document ID, and the Hono context (c).
repos: {
events: {
path: "events",
rules: [
{
description: "Cannot cancel an event once invoiced",
run: ({ before, changes }) => {
if (changes.status === "cancelled" && before.isInvoiced) {
return false; // Rejects with 403 and the rule description
}
return true;
},
},
{
description: "Only admins can change event ownership",
run: ({ changes, c }) => {
if (changes.organizerId) {
const user = c.get("user");
if (user?.role !== "admin") return "Forbidden: admin role required";
}
return true;
},
},
],
},
}If a rule returns false or a string error message, the mutation is rejected with HTTP 403 Forbidden without writing to Firestore.
Missing Index Detection (indexesError & HTTP 424)
When a complex filter or compound query requires a composite index that has not yet been built in Firestore, the CRUD server automatically catches the Firestore FAILED_PRECONDITION error:
- Invokes
indexesErrorcallback: Passes{ repoName, error, indexUrl, c }for error tracking, alerting, or Sentry logging. - Returns
HTTP 424 Failed Dependency: Includes the direct link to the Firebase Console index creator in the JSON response:
{
"success": false,
"error": "The query requires a composite index.",
"indexUrl": "https://console.firebase.google.com/v1/r/project/my-project/firestore/indexes?create_composite=..."
}