Skip to content

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) with where, orWhere, orWhereGroups, and includes
  • 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

typescript
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

MethodEndpointDescription
GET/:repoNameList documents (paginated, with query filters and withTotal)
GET/:repoName/:idGet single document by ID
POST/:repoNameCreate a new document (Zod validated)
PUT/:repoName/:idReplace document
PATCH/:repoName/:idPartial update document
DELETE/:repoName/:idDelete document
POST/:repoName/queryAdvanced query (AND, OR groups, includes, total count)
POST/:repoName/batchExecute atomic batch write operations
GET/openapi.jsonOpenAPI 3.1 JSON specification
GET/docsInteractive Scalar API documentation

Listing documents (GET /:repoName)

Query parameters:

  • pageSize: Number of items per page (default: 25, max: 100)
  • cursor: Base64 cursor token for pagination
  • direction: Pagination direction (next | prev)
  • orderBy: Field to sort by
  • orderDir: Sort direction (asc | desc)
  • select: Comma-separated list of fields to project
  • includes: Comma-separated list of relations to populate
  • withTotal: Set to true to 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:

json
{
  "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:

json
{
  "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 in clauses: Uses server-side .count() aggregation. totalCountIsExact is true.
  • OR queries (orWhere, orWhereGroups): Executes parallel server-side .count() queries across OR branches. Fast and lightweight (1000 docs = 1 index read). totalCountIsExact is false (estimated due to potential branch overlap).

Configuration options (CrudServerOptions)

OptionTypeDescription
basePathstringBase path prefix (e.g., "/api/v1")
middlewaresMiddlewareHandler[]Custom Hono middleware array
reposRecord<string, CrudRepoConfig>Per-repository configuration mapping
indexesError(err: { repoName, error, indexUrl, c }) => voidCallback triggered when a Firestore missing-index error occurs
openapiOpenAPISpecOptionsOpenAPI 3.1 documentation settings
verbosebooleanInclude detailed error messages in HTTP 500 responses

Per-Repository Configuration (CrudRepoConfig)

OptionTypeDescription
pathstringCollection path
filterableFieldsstring[]List of allowed fields for filtering
orderableFieldsstring[]List of allowed fields for ordering
allowedIncludesstring[]List of relation keys allowed in includes
allowDeletebooleanWhether DELETE /:id is enabled
rulesCrudRule[]Before rules & diff engine for mutation validation
pageSizenumberDefault 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).

typescript
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:

  1. Invokes indexesError callback: Passes { repoName, error, indexUrl, c } for error tracking, alerting, or Sentry logging.
  2. Returns HTTP 424 Failed Dependency: Includes the direct link to the Firebase Console index creator in the JSON response:
json
{
  "success": false,
  "error": "The query requires a composite index.",
  "indexUrl": "https://console.firebase.google.com/v1/r/project/my-project/firestore/indexes?create_composite=..."
}