Documentation
Configuration

Configuration

Learn how to configure Omni Rest for your specific needs.

Basic Configuration

Only expose specific models:

import { expressAdapter } from "omni-rest/express";
 
expressAdapter(prisma, {
  allow: ["product", "category"],  // Only these models are exposed
});

Authorization Guards

Control access to operations per model:

expressAdapter(prisma, {
  guards: {
    product: {
      POST: async ({ id, body }) => {
        if (!isAdmin()) {
          return "Only admins can create products";
        }
      },
      DELETE: async ({ id, body }) => {
        if (!isAdmin()) {
          return "Only admins can delete products";
        }
      },
      PATCH: async ({ id, body }) => {
        if (!isModerator()) {
          return "Only moderators can update products";
        }
      },
    },
    category: {
      DELETE: async ({ id, body }) => {
        // Prevent deleting categories with products
        const count = await prisma.product.count({
          where: { categoryId: id },
        });
        if (count > 0) {
          return "Cannot delete categories with products";
        }
      },
    },
  },
});
⚠️

Guard Context:
id: Record ID (for single operations)
body: Request body data

Return Value:
Return an error message string to deny access, or return undefined/null to allow.

Automatic Request Validation

Omni Rest provides an incredibly powerful, out-of-the-box guard called withValidation(). By simply wrapping your guards with it, Omni Rest will automatically intercept and validate all incoming POST/PUT/PATCH payloads against strict Zod schemas generated precisely from your database models.

import { expressAdapter, withValidation } from "omni-rest";
 
expressAdapter(prisma, {
  // Automatically protects all write-endpoints from invalid JSON bodies!
  guards: withValidation(), 
});

You can effortlessly combine withValidation with your specific custom guards:

expressAdapter(prisma, {
  guards: withValidation({
    product: {
      DELETE: async ({ id }) => {
        if (!isAdmin()) return "Admins only";
      }
    }
  }),
});

Lifecycle Hooks

Run code before/after operations:

expressAdapter(prisma, {
  beforeOperation: async ({ model, method, id, body }) => {
    // Log all operations
    console.log(`${method} ${model}${id ? `/${id}` : ""}`);
 
    // Add audit trail
    await auditLog.create({
      model,
      method,
      timestamp: new Date(),
      userId: currentUser?.id,
    });
  },
 
  afterOperation: async ({ model, method, id, body, result }) => {
    // Send webhooks
    if (method === "POST") {
      await webhook.trigger("record.created", {
        model,
        data: result,
      });
    }
 
    // Update cache
    cache.invalidate(model);
  },
});

Hook Context:
model: Model name
method: HTTP method (GET, POST, PUT, PATCH, DELETE)
id: Record ID (for single operations)
body: Request body
result: Operation result (afterOperation only)

Response Envelope

By default, list endpoints return { data, meta }. Set envelope: false to get a plain array instead — useful for frontend libraries that expect a flat response.

expressAdapter(prisma); // envelope defaults to true
GET /api/department
→ {
    "data": [...],
    "meta": { "total": 42, "page": 1, "limit": 20, "totalPages": 3 }
  }

Soft Delete

When enabled, DELETE updates a designated field instead of destroying the record. The model's fields are auto-detected from your Prisma schema — no extra config needed in most cases.

omni-rest looks for deletedAt (DateTime) or isActive (Boolean) automatically:

expressAdapter(prisma, {
  softDelete: true,
});
DELETE /api/post/5
→ UPDATE Post SET deletedAt = NOW() WHERE id = 5
→ 200 { id: 5, deletedAt: "2026-03-31T..." }

GET list automatically excludes soft-deleted records:

GET /api/post
→ WHERE deletedAt IS NULL

If the model has no matching soft-delete field, omni-rest falls back to a hard delete automatically — no error thrown.

OpenAPI Configuration

Generate customized API documentation and serve it natively:

import { generateOpenApiSpec } from "omni-rest";
import swaggerUi from "swagger-ui-express";
 
const spec = generateOpenApiSpec(prisma, {
  title: "My Company API",
  version: "1.0.0",
  basePath: "/api",
  allow: ["product", "category"],  // Only document these models
  servers: [
    { url: "https://api.example.com", description: "Production" },
    { url: "https://staging-api.example.com", description: "Staging" },
  ],
});
 
app.get("/openapi.json", (_, res) => res.json(spec));
app.use("/docs", swaggerUi.serve, swaggerUi.setup(spec, {
  swaggerOptions: {
    persistAuthorization: true,
    displayOperationId: true,
  },
}));

Per-Adapter Configuration

import { expressAdapter } from "omni-rest/express";
 
app.use("/api", expressAdapter(prisma, options));

Rate Limiting

Add a rateLimit function to block requests before they reach guards or Prisma. Return an error string to respond with 429, or null to allow.

expressAdapter(prisma, {
  rateLimit: async ({ model, method, id }) => {
    const key = `${model}:${method}`;
    const count = await redis.incr(key);
    await redis.expire(key, 60); // 1-minute window
    if (count > 100) return "Too many requests. Try again later.";
    return null;
  },
});

The context object contains:

PropertyTypeDescription
modelstringPrisma model name (e.g. "User")
methodstringHTTP method (GET, POST, etc.)
idstring | nullRecord ID for single-record operations

Rate limiting runs after model resolution but before guards. It is not called for unknown model routes (those return 404 immediately).

Field-Level Permissions

Control which fields are readable and writable per model without writing custom Prisma queries.

expressAdapter(prisma, {
  fieldGuards: {
    user: {
      hidden:    ["salt", "internalNote"],  // never in any response
      readOnly:  ["id", "createdAt"],       // stripped from POST/PUT/PATCH bodies
      writeOnly: ["password"],              // accepted in writes, never returned in GET
    },
  },
});
OptionEffect
hiddenRemoved from all responses (GET single, GET list, POST/PUT response)
readOnlyStripped from write bodies before reaching Prisma
writeOnlyRemoved from all GET responses; accepted in write bodies
⚠️

hidden and writeOnly both suppress fields from responses. Use hidden for fields that should never be exposed at all (e.g. salt). Use writeOnly for fields that are accepted on write but should not be echoed back (e.g. password).

Global Error Handler (Express)

omniRestErrorHandler() is a drop-in Express error middleware that maps Prisma error codes to clean JSON responses — covering both omni-rest routes and your own custom routes.

import { expressAdapter, omniRestErrorHandler } from "omni-rest/express";
 
app.use(express.json());
app.use("/api", expressAdapter(prisma));
app.use("/custom", myCustomRoutes);
 
// Mount last — catches Prisma errors from all routes above
app.use(omniRestErrorHandler());
Prisma codeHTTP statusResponse
P2025404{ error: "Record not found." }
P2002409{ error: "Unique constraint failed on: <field>" }
P2003400{ error: "Foreign key constraint failed." }
P2014400{ error: "Relation violation." }
Other Prisma500{ error: "<message>" }
Non-PrismaPassed to next(err)
⚠️

Mount omniRestErrorHandler() after all your routes, not before. Express error middleware must have exactly 4 arguments (err, req, res, next) to be recognized as an error handler.

Best Practices

Always add guards for writes

Control who can create/delete sensitive records globally.

Log all operations

Use hooks for comprehensive audit trails.

Set reasonable limits

Prevent abuse with the maxLimit configuration.

Document your API

Always mount the generated OpenAPI spec for your clients.