Contracts - Schemas & Types
Radish CLI generates Zod schemas and TypeScript types for runtime validation and compile-time type safety.
Generated Files
For each entity, Radish generates a contract file in .radish/lib/datalayer/contracts/:
contracts/
├── common.ts # Base schemas and types
├── user.ts # User entity contracts
├── task.ts # Task entity contracts
└── index.ts # Barrel export
Schema Naming Convention
Radish uses a Schema suffix naming pattern to clearly distinguish runtime validators from compile-time types:
- Zod schemas:
camelCase+Schemasuffix (e.g.,taskSchema,createTaskSchema) - TypeScript types:
PascalCasewithout suffix (e.g.,Task,CreateTask)
This pattern follows industry standards used by tRPC, Remix, and Prisma.
Generated Schemas Per Entity
For each entity, Radish generates:
1. Fields Schema
Business fields only (excludes base fields).
export const taskFieldsSchema = z.object({
title: z.string(),
description: z.string().optional(),
status: z.enum(['todo', 'in_progress', 'done']),
dueDate: z.string().datetime().optional(),
}).strict();
export type TaskFields = z.infer<typeof taskFieldsSchema>;
2. Full Entity Schema
Complete entity with base fields merged in.
export const taskSchema = taskFieldsSchema.merge(entityBaseSchema).strict();
export type Task = z.infer<typeof taskSchema>;
3. Create Schema
Fields required for creating a new entity.
export const createTaskSchema = z.object({
title: z.string(),
description: z.string().optional(),
status: z.enum(['todo', 'in_progress', 'done']).optional(),
dueDate: z.string().datetime().optional(),
}).strict();
export type CreateTask = z.infer<typeof createTaskSchema>;
4. Update Schema
Fields allowed when updating (all optional).
export const updateTaskSchema = z.object({
title: z.string().optional(),
description: z.string().optional(),
status: z.enum(['todo', 'in_progress', 'done']).optional(),
dueDate: z.string().datetime().optional(),
}).strict();
export type UpdateTask = z.infer<typeof updateTaskSchema>;
5. Criteria Schema
Query parameters for filtering and pagination.
export const taskCriteriaSchema = criteriaBaseSchema.extend({
ownerId: z.string().optional(),
status: z.enum(['todo', 'in_progress', 'done']).optional(),
dueDate: z.string().datetime().optional(),
}).strict();
export type TaskCriteria = z.infer<typeof taskCriteriaSchema>;
Base Schemas
Common Types
The common.ts file exports base schemas used across all entities:
// Basic types
export const objectIdStrSchema = z.string().regex(/^[a-f0-9]{24}$/, "ObjectId hex");
export const isoDateSchema = z.string().datetime();
// Base entity schemas
export const entityBaseSchema = z.object({
id: objectIdStrSchema,
ownerId: z.string(),
createdAt: isoDateSchema,
updatedAt: isoDateSchema
});
export const systemEntityBaseSchema = z.object({
id: objectIdStrSchema,
createdAt: isoDateSchema,
updatedAt: isoDateSchema
});
// Content base (extends entityBase)
export const contentBaseSchema = entityBaseSchema.extend({
title: z.string(),
slug: z.string().optional(),
summary: z.string().optional(),
tags: z.array(z.string()).default([]),
status: z.enum(['draft', 'published', 'archived']).default('draft'),
publishedAt: isoDateSchema.optional(),
featuredImage: z.string().optional()
});
// TypeScript types
export type ObjectIdStr = z.infer<typeof objectIdStrSchema>;
export type ISODate = z.infer<typeof isoDateSchema>;
export type EntityBase = z.infer<typeof entityBaseSchema>;
export type SystemEntityBase = z.infer<typeof systemEntityBaseSchema>;
export type ContentBase = z.infer<typeof contentBaseSchema>;
Criteria Base
Query parameter schema with pagination, sorting, and filtering:
export const criteriaBaseSchema = z.object({
q: z.string().optional(), // Text search
limit: z.coerce.number().int().min(1).max(200).optional(),
cursor: z.string().optional(), // Cursor pagination
includeArchived: z.coerce.boolean().optional(),
sort: z.object({
field: z.string(),
direction: z.enum(['asc', 'desc']),
collation: collationSchema.optional()
}).optional(),
populate: z.union([
z.string(), // "authorId,categoryId"
z.array(z.string()), // ["authorId", "categoryId"]
z.object({
paths: z.array(z.string()),
depth: z.coerce.number().int().min(1).max(5).optional()
})
]).optional()
}).strict();
export type CriteriaBase = z.infer<typeof criteriaBaseSchema>;
Usage Examples
Validating User Input
import { createTaskSchema } from '@generated/datalayer/contracts';
// Validate and parse user input
const input = createTaskSchema.parse(req.body);
// Safe parse (doesn't throw)
const result = createTaskSchema.safeParse(req.body);
if (!result.success) {
console.error(result.error);
}
Type Annotations
import type { Task, CreateTask, TaskCriteria } from '@generated/datalayer/contracts';
function processTask(task: Task) {
console.log(task.title);
}
async function createTask(input: CreateTask): Promise<Task> {
// ...
}
function findTasks(criteria: TaskCriteria): Promise<Task[]> {
// ...
}
Runtime Validation
import { taskSchema, updateTaskSchema } from '@generated/datalayer/contracts';
// Validate complete entity
const task = taskSchema.parse(dbRecord);
// Validate partial update
const updates = updateTaskSchema.parse(req.body);
Form Validation
import { createTaskSchema } from '@generated/datalayer/contracts';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
const form = useForm({
resolver: zodResolver(createTaskSchema)
});
Enum Generation
For enum fields with enhanced format (key-label pairs), Radish generates TypeScript enums:
// Blueprint:
// status:
// type: enum
// values:
// - key: todo
// label: To Do
// - key: in_progress
// label: In Progress
// Generated:
export enum TaskStatusEnum {
"todo" = "To Do",
"in_progress" = "In Progress",
"done" = "Done"
}
Versioning Contracts
If versioning is enabled, additional contracts are generated:
Full Versioning
export const taskVersionSchema = z.object({
id: z.string(),
entityId: z.string(),
version: z.number().int(),
snapshot: z.any(),
changedBy: z.string(),
changeReason: z.string().optional(),
changeType: z.enum(['create', 'update', 'delete', 'archive', 'restore']),
changedFields: z.array(z.string()).optional(),
createdAt: z.string(),
updatedAt: z.string(),
}).strict();
export type TaskVersion = z.infer<typeof taskVersionSchema>;
Simple Versioning (Audit Log)
export const taskAuditLogSchema = z.object({
id: z.string(),
entityId: z.string(),
changedBy: z.string(),
changeType: z.enum(['create', 'update', 'delete', 'archive', 'restore']),
changedFields: z.array(z.string()).optional(),
changes: z.record(z.object({
old: z.any(),
new: z.any(),
})).optional(),
changeReason: z.string().optional(),
createdAt: z.string(),
updatedAt: z.string(),
}).strict();
export type TaskAuditLog = z.infer<typeof taskAuditLogSchema>;
Next Steps
- Services API - Using the generated service layer
- Repositories - Data access layer
- HTTP Routes - REST API endpoints