@zooly/util-srv is a server-side utility package that provides common server-only functions and services used across server packages in the monorepo.
This package is server-only and should not be used in client-side code. It contains AWS SDK dependencies and other server-specific functionality.
@zooly/util-srvpackages/util-srvroute() builder — the standard wrapper for every offer-related API endpoint (auth, CORS, Zod validation, idempotency, error envelope)resolveAccountId, assertAdmin, isAdminwithIdempotency for retry-safe writesroute() builderA single wrapper applied to every offer endpoint. Folds in authentication, CORS, Zod-validated query/body parsing, idempotency, and a structured error envelope, so the route file itself contains only business logic.
import { route } from "@zooly/util-srv";
import { listNotificationsQuerySchema } from "@zooly/contracts";
import { listNotifications, getUnreadCount } from "@zooly/db";
export const { GET, OPTIONS } = route({
method: "GET",
auth: "user", // "none" | "user" | "admin"
cors: { methods: ["GET"] }, // opt-in — omit for no CORS
querySchema: listNotificationsQuerySchema,
handler: async ({ accountId, query }) => {
const [notifications, unreadCount] = await Promise.all([
listNotifications(accountId, query.limit),
getUnreadCount(accountId),
]);
return { body: { notifications, unreadCount } };
},
});
| Option | Type | Purpose |
|---|---|---|
method | "GET" | "POST" | "PATCH" | "PUT" | "DELETE" | HTTP method the endpoint responds to |
auth | "none" | "user" | "admin" | Auth mode. "user" and "admin" populate ctx.accountId, ctx.userId, ctx.isAdmin, ctx.user |
cors | { methods: HttpMethod[] } | undefined | Opt-in. When set, CORS headers are applied and an OPTIONS handler is returned. Omitted routes (e.g. payments/create-intent) behave as they did before |
querySchema | ZodType | Validates request.nextUrl.searchParams. Coerces numeric query strings |
bodySchema | ZodType | Validates the request body JSON. Invalid JSON → 400 BadRequest |
paramsSchema | ZodType | Validates Next dynamic route params (e.g. [id]). Failure → 400 BadRequest |
idempotent | boolean | When true and method !== "GET", reads Idempotency-Key header and replays cached results via withIdempotency |
handler | (ctx) => Promise<{ body, status? }> | The business logic. Returns { body, status? }; status defaults to 200 |
| Field | Present when | Description |
|---|---|---|
request | always | The raw NextRequest |
query | querySchema set | Parsed query object |
body | bodySchema set and method ≠ GET | Parsed body object |
params | paramsSchema set (for [id]-style routes) | Parsed route params |
accountId | auth ≠ "none" | Authenticated user's account id |
userId | auth ≠ "none" | Authenticated user id |
isAdmin | auth ≠ "none" | Whether the user has the admin role |
user | auth ≠ "none" | The full User record (id, email, name, roles) — useful when a downstream service needs the user's name or email |
Throw an HttpError from @zooly/util-srv with an ErrorCode from @zooly/contracts:
import { HttpError } from "@zooly/util-srv";
if (!isAdmin) throw new HttpError("Forbidden", "Admin access required");
// With structured details (e.g. rate-limit counts)
throw new HttpError("RateLimited", "Daily cap reached", {
currentCount: 5,
limit: 5,
});
The details argument lands in the response body as { error, code, details } and flows through to @zooly/api-client consumers via the Result<T> details field. The builder also maps legacy { status: 4xx } thrown errors to the matching ErrorCode, for backward compatibility with pre-migration helpers.
Every non-2xx response from a route()-wrapped endpoint is:
{ "error": "Human-readable message", "code": "Forbidden" }
See the contracts package for the full ErrorCode list.
CORS is opt-in. When cors is provided, allowed origins come from the ALLOWED_DOMAINS_CORS environment variable (comma-separated). Required for talent app cross-origin: must include http://localhost:3007 locally.
Wraps the pre-existing withIdempotency helper. Clients set Idempotency-Key: <uuid> on writes; the builder caches the first successful (2xx) response and replays it on any retry with the same key. GET requests never consult the idempotency store.
resolveAccountId(cookieHeader, targetAccountId?)Authenticates the cookie, fetches the user's account, and returns { accountId, userId }. If targetAccountId is passed and differs from the user's own account, requires the admin role or throws a 403.
assertAdmin(cookieHeader)Like resolveAccountId but fails closed unless the user has the admin role.
isAdmin(user)Pure check: user.roles?.includes("admin").
withIdempotency(key, handler)Low-level helper used by route(). Given a nullable idempotency key and a handler:
null key → runs the handler directly (no caching).{ body, status } without running the handler.Underlying storage is the idempotency_keys table in @zooly/db.
The package provides comprehensive S3 functionality:
The package provides OpenAI embedding generation functionality:
generateEmbedding(text) - Generate embedding vectors for text content
text-embedding-3-small modelpackages/util-srv/src/generateEmbedding.ts for implementationThe package provides AI-powered structured data extraction from natural language:
generateStructuredData(prompt, schema, schemaName?, schemaDescription?, output?) - Extract structured data from text using Google Gemini
gemini-2.5-flash-lite model"object", "enum", "array", or "no-schema"packages/util-srv/src/generateStructuredData.ts for implementationimport { generateStructuredData } from "@zooly/util-srv";
import { z } from "zod";
const schema = z.object({
category: z.enum(["MODELS", "ACTORS", "MUSICIANS"]).optional(),
gender: z.enum(["MALE", "FEMALE", "NON_BINARY"]).optional(),
hairColor: z.enum(["BLONDE", "BROWN", "BLACK", "RED"]).optional(),
});
const result = await generateStructuredData(
"Looking for a tall blonde female model in the US",
schema,
"SearchFilters",
"Extract search characteristics from description"
);
// Result: { category: "MODELS", gender: "FEMALE", hairColor: "BLONDE" }
This function is also exposed via the API endpoint /api/generate-structured-data for client-side usage. The endpoint accepts a plain object schema definition (serialized Zod schema) and returns the extracted structured data.
next for NextRequest / NextResponse types used by the route builder@aws-sdk/client-s3 for S3 operations@zooly/contracts for the shared error-code enum consumed by route()@zooly/db for idempotency storage and account lookup@zooly/types for shared types@zooly/util for shared utilities (cookie-based user verification)nanoid for generating unique IDsai, @ai-sdk/openai, and @ai-sdk/google for AI operations (embeddings and structured data generation)zod for schema validationThis package provides server-side utilities that can be imported by server packages in the monorepo. It's particularly useful for file storage operations and AWS integrations.
The package requires the following environment variables:
ALLOWED_DOMAINS_CORS — comma-separated list of origins permitted by route() when cors is opted in. Must include http://localhost:3007 for the talent app during local dev.AWS_REGION - AWS region for S3 operationsAWS_ACCESS_KEY_ID - AWS access key IDAWS_SECRET_ACCESS_KEY - AWS secret access keyAWS_BUCKET_NAME - S3 bucket nameOPENAI_API_KEY - OpenAI API key for embedding generationGOOGLE_GENERATIVE_AI_API_KEY - Google AI API key for structured data generation (Gemini)cd packages/util-srv
pnpm test
21 tests cover the route() builder across auth, validation, CORS, params, idempotency, and error-mapping paths (including HttpError.details pass-through and async [id] param parsing).
On This Page
OverviewPackage DetailsKey Features[object Object], builderConfigHandler contextThrowing errors from a handlerError envelopeCORSIdempotencyAuth helpers[object Object][object Object][object Object]Idempotency[object Object]S3 UtilitiesEmbedding GenerationStructured Data GenerationUsage ExampleUse CasesAPI EndpointDependenciesUsageEnvironment VariablesTesting