For the complete documentation index, see llms.txt. Prefer markdown by appending.mdto documentation URLs or sendingAccept: text/markdown.
Data validation
Validate every untrusted query, body, and param with Zod before handlers run. Use the shared validate middleware and keep schemas next to your API modules.
Authentication tells you who is calling. Validation decides whether the payload is safe to use.
TurboStarter validates API input with Zod through a shared Hono middleware. Invalid data never reaches business logic.
Validate at the edge of the API
Use validate from packages/api/src/middleware.ts on query, JSON body, or path params:
import { enforceAuth, validate } from "../../middleware";
import { getObjectUrlSchema } from "@workspace/storage/server";
export const storageRouter = new Hono().get(
"/upload",
enforceAuth,
validate("query", getObjectUrlSchema),
async (c) => c.json(await getUploadUrl(c.req.valid("query"))),
);Inside the handler, read c.req.valid("query") / c.req.valid("json") - typed, already parsed, and rejected with 422 when invalid.
Schemas live next to the domain (for example @workspace/api/schema, @workspace/billing-web/schema, storage schemas) so the same rules can be reused on the client for forms when useful.
What to validate
Validate everything that comes from outside your process:
- Query strings and route params
- JSON bodies
- Headers you interpret as data (not cookies Better Auth already verifies)
- Webhook payloads after signature verification
- File metadata (content type, size limits) before issuing upload URLs
Do not trust:
- Client-provided roles or
isAdminflags - Client-provided prices or plan ids for granting entitlements
- Unbounded strings that land in SQL-like filters, emails, or redirects
Drizzle uses parameterized queries, which protects against classic SQL injection when you stick to the query builder. Validation still matters for logic bugs, mass assignment, and abuse (oversized payloads, unexpected enums).
Prefer allowlists
Model inputs as the smallest shape you need:
const createProjectSchema = z.object({
name: z.string().trim().min(1).max(80),
organizationId: z.string().min(1),
});Avoid “pass-through” objects like z.record(z.unknown()) for mutation endpoints. Extra fields should not silently update privileged columns.
i18n-aware errors
The shared validate middleware maps Zod issues through the i18n layer so clients get localized messages instead of raw English schema errors. Keep using it instead of ad-hoc schema.parse in routers unless you have a reason (for example webhooks that need a different error shape).
Forms and Server Actions
Mirror the same schemas in UI forms (react-hook-form + Zod, or your preferred stack). Re-validate on the server anyway - client validation is UX, server validation is security.
"use server";
export async function createProject(input: unknown) {
const data = createProjectSchema.parse(input);
const { user } = await getSession();
if (!user) {
throw new Error("Unauthorized");
}
// enforce membership for data.organizationId, then insert
}How is this guide?
Last updated on