Util Server Package

Server-side utility functions and services

Overview

@zooly/util-srv is a server-side utility package that provides common server-only functions and services used across server packages in the monorepo.

Package Details

  • Package Name: @zooly/util-srv
  • Location: packages/util-srv
  • Type: Server-side utility library

Key Features

  • route() builder — the standard wrapper for every offer-related API endpoint (auth, CORS, Zod validation, idempotency, error envelope)
  • Auth helpersresolveAccountId, assertAdmin, isAdmin
  • IdempotencywithIdempotency for retry-safe writes
  • S3 operations (upload, download, existence checks)
  • Image upload utilities
  • Embedding generation for AI/ML applications
  • Structured data extraction from natural language using AI

route() builder

A 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 } };
  },
});

Config

OptionTypePurpose
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[] } | undefinedOpt-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
querySchemaZodTypeValidates request.nextUrl.searchParams. Coerces numeric query strings
bodySchemaZodTypeValidates the request body JSON. Invalid JSON → 400 BadRequest
paramsSchemaZodTypeValidates Next dynamic route params (e.g. [id]). Failure → 400 BadRequest
idempotentbooleanWhen 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

Handler context

FieldPresent whenDescription
requestalwaysThe raw NextRequest
queryquerySchema setParsed query object
bodybodySchema set and method ≠ GETParsed body object
paramsparamsSchema set (for [id]-style routes)Parsed route params
accountIdauth ≠ "none"Authenticated user's account id
userIdauth ≠ "none"Authenticated user id
isAdminauth ≠ "none"Whether the user has the admin role
userauth ≠ "none"The full User record (id, email, name, roles) — useful when a downstream service needs the user's name or email

Throwing errors from a handler

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.

Error envelope

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

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.

Idempotency

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.

Auth helpers

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").

Idempotency

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).
  • key already stored → returns the cached { body, status } without running the handler.
  • key not stored → runs the handler, stores successful (2xx) results, returns the fresh result.

Underlying storage is the idempotency_keys table in @zooly/db.

S3 Utilities

The package provides comprehensive S3 functionality:

  • Upload to S3: Upload buffers to S3 and get public URLs
  • Get S3 Object: Download objects from S3 as byte arrays
  • Check Object Exists: Verify if an S3 object exists and get its URL
  • Upload Image from URL: Fetch images from URLs and upload them to S3
  • S3 URL Helpers: Check if a URL is an S3 URL and build public URLs

Embedding Generation

The package provides OpenAI embedding generation functionality:

  • generateEmbedding(text) - Generate embedding vectors for text content
    • Uses OpenAI's text-embedding-3-small model
    • Returns 1536-dimensional embedding vectors
    • Validates input text and embedding dimensions
    • See packages/util-srv/src/generateEmbedding.ts for implementation

Structured Data Generation

The package provides AI-powered structured data extraction from natural language:

  • generateStructuredData(prompt, schema, schemaName?, schemaDescription?, output?) - Extract structured data from text using Google Gemini
    • Uses Google Gemini gemini-2.5-flash-lite model
    • Takes a natural language prompt and a Zod schema
    • Returns validated structured data matching the schema
    • Supports output modes: "object", "enum", "array", or "no-schema"
    • Automatically validates and type-checks the AI response
    • Logs warnings for empty string values in the response
    • See packages/util-srv/src/generateStructuredData.ts for implementation

Usage Example

import { 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" }

Use Cases

  • Search Filter Extraction: Convert natural language search queries into structured filter objects
  • Form Data Parsing: Extract structured data from user descriptions or briefs
  • Tag Extraction: Parse unstructured text into validated tag objects
  • Data Normalization: Convert free-form text into standardized enum values

API Endpoint

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.

Dependencies

  • 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 IDs
  • ai, @ai-sdk/openai, and @ai-sdk/google for AI operations (embeddings and structured data generation)
  • zod for schema validation

Usage

This 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.

Environment Variables

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 operations
  • AWS_ACCESS_KEY_ID - AWS access key ID
  • AWS_SECRET_ACCESS_KEY - AWS secret access key
  • AWS_BUCKET_NAME - S3 bucket name
  • OPENAI_API_KEY - OpenAI API key for embedding generation
  • GOOGLE_GENERATIVE_AI_API_KEY - Google AI API key for structured data generation (Gemini)

Testing

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).