Input/output schemas
Use Zod schemas to validate incoming requests and document expected responses. This provides runtime validation, compile-time type safety, and automatic OpenAPI schema generation.
Why schemas?
Schemas provide:
- Runtime validation - Reject invalid requests before query execution
- Type safety - Full TypeScript inference from Zod to your handlers
- Auto-documentation - OpenAPI schemas generated automatically
- Error handling - Detailed validation errors returned to clients
- AI agent compatibility - JSON Schema for tool discovery
Input schemas
Define what data your endpoint accepts:
import { defineServe } from '@hypequery/serve';
import { z } from 'zod';
const api = defineServe({
queries: {
revenue: {
inputSchema: z.object({
startDate: z.string().datetime(),
endDate: z.string().datetime(),
currency: z.enum(['USD', 'EUR', 'GBP']).default('USD'),
includeRefunds: z.boolean().optional(),
}),
query: async ({ input }) => {
// input is fully typed based on inputSchema!
const { startDate, endDate, currency, includeRefunds } = input;
return db.table('transactions')
.where('date', '>=', startDate)
.where('date', '<=', endDate)
.where('currency', currency)
.sum('amount');
},
},
},
});Validation errors
Invalid requests return detailed error messages:
{
"error": {
"type": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": {
"issues": [
{
"code": "invalid_type",
"expected": "string",
"received": "number",
"path": ["startDate"],
"message": "Expected string, received number"
}
]
}
}
}Output schemas
Document what your endpoint returns:
Output schemas are optional for in-process usage. If you omit them, TypeScript infers the return type directly from your resolver and api.run() remains fully typed. However, without an outputSchema, OpenAPI docs, api.describe(), and external agents won’t see structured response metadata. Add one whenever the query is exposed outside your codebase.
queries: {
revenue: {
inputSchema: z.object({
startDate: z.string(),
endDate: z.string(),
}),
outputSchema: z.object({
total: z.number(),
currency: z.string(),
breakdown: z.array(z.object({
date: z.string(),
amount: z.number(),
transactionCount: z.number(),
})),
metadata: z.object({
generatedAt: z.string(),
queryDurationMs: z.number(),
}),
}),
query: async ({ input }) => {
// Return value is type-checked against outputSchema
return {
total: 42000,
currency: 'USD',
breakdown: [
{ date: '2025-01-01', amount: 10000, transactionCount: 50 },
{ date: '2025-01-02', amount: 12000, transactionCount: 60 },
],
metadata: {
generatedAt: new Date().toISOString(),
queryDurationMs: 123,
},
};
},
},
}Common schema patterns
Date ranges
const dateRangeSchema = z.object({
startDate: z.string().datetime(),
endDate: z.string().datetime(),
}).refine(
(data) => new Date(data.startDate) < new Date(data.endDate),
{ message: 'startDate must be before endDate' }
);
queries: {
metrics: {
inputSchema: dateRangeSchema,
query: async ({ input }) => { /* ... */ },
},
}Unions and discriminated unions
const reportSchema = z.discriminatedUnion('type', [
z.object({
type: z.literal('revenue'),
currency: z.enum(['USD', 'EUR']),
includeRefunds: z.boolean(),
}),
z.object({
type: z.literal('users'),
includeInactive: z.boolean(),
segment: z.enum(['free', 'paid', 'enterprise']),
}),
z.object({
type: z.literal('performance'),
metric: z.enum(['latency', 'throughput', 'errors']),
percentile: z.number().min(50).max(99),
}),
]);
queries: {
generateReport: {
inputSchema: reportSchema,
query: async ({ input }) => {
switch (input.type) {
case 'revenue':
return generateRevenueReport(input.currency, input.includeRefunds);
case 'users':
return generateUserReport(input.includeInactive, input.segment);
case 'performance':
return generatePerformanceReport(input.metric, input.percentile);
}
},
},
}Reusable schemas
Define shared schemas once and reuse them:
// schemas/common.ts
import { z } from 'zod';
export const dateRangeSchema = z.object({
startDate: z.string().datetime(),
endDate: z.string().datetime(),
});
export const currencySchema = z.enum(['USD', 'EUR', 'GBP', 'JPY']);
export const paginationInputSchema = z.object({
page: z.number().int().positive().default(1),
pageSize: z.number().int().min(1).max(100).default(20),
});
export const paginationOutputSchema = <T extends z.ZodTypeAny>(dataSchema: T) =>
z.object({
data: z.array(dataSchema),
pagination: z.object({
page: z.number(),
pageSize: z.number(),
totalPages: z.number(),
totalCount: z.number(),
}),
});
// api/index.ts
import { dateRangeSchema, currencySchema, paginationOutputSchema } from './schemas/common';
const userSchema = z.object({
id: z.string(),
email: z.string(),
name: z.string(),
});
const api = defineServe({
queries: {
revenue: {
inputSchema: dateRangeSchema.extend({
currency: currencySchema,
}),
query: async ({ input }) => { /* ... */ },
},
users: {
inputSchema: paginationInputSchema,
outputSchema: paginationOutputSchema(userSchema),
query: async ({ input }) => { /* ... */ },
},
},
});Type inference
TypeScript automatically infers types from your schemas:
const userSchema = z.object({
id: z.string(),
email: z.string().email(),
name: z.string(),
age: z.number().int().positive().optional(),
});
type User = z.infer<typeof userSchema>;
// type User = {
// id: string;
// email: string;
// name: string;
// age?: number | undefined;
// }
queries: {
createUser: {
inputSchema: userSchema,
outputSchema: userSchema.extend({
createdAt: z.string().datetime(),
}),
query: async ({ input }) => {
// input: User (fully typed!)
const user = await db.table('users').insert(input).returning('*');
return {
...user,
createdAt: new Date().toISOString(),
};
},
},
}OpenAPI generation
Schemas automatically generate OpenAPI documentation:
queries: {
revenue: {
inputSchema: z.object({
startDate: z.string().datetime().describe('Start of date range (ISO 8601)'),
endDate: z.string().datetime().describe('End of date range (ISO 8601)'),
currency: z.enum(['USD', 'EUR']).default('USD').describe('Currency code'),
}),
outputSchema: z.object({
total: z.number().describe('Total revenue in specified currency'),
transactionCount: z.number().int().describe('Number of transactions'),
}),
summary: 'Get revenue for date range',
description: 'Returns total revenue and transaction count',
query: async ({ input }) => { /* ... */ },
},
}
// Auto-generates OpenAPI spec:
// {
// "/metrics/revenue": {
// "get": {
// "summary": "Get revenue for date range",
// "description": "Returns total revenue and transaction count",
// "parameters": [
// {
// "name": "startDate",
// "in": "query",
// "description": "Start of date range (ISO 8601)",
// "required": true,
// "schema": { "type": "string", "format": "date-time" }
// },
// ...
// ],
// "responses": {
// "200": {
// "description": "Successful response",
// "content": {
// "application/json": {
// "schema": {
// "type": "object",
// "properties": {
// "total": { "type": "number", "description": "Total revenue..." },
// "transactionCount": { "type": "integer", "description": "Number of..." }
// }
// }
// }
// }
// }
// }
// }
// }
// }