A SvelteKit application demonstrating true single-source-of-truth architecture where Zod schemas define validation, types, and API documentation in one place.
π Quick Start Guide | π Development Workflow | π§ Troubleshooting | π Swagger UI Guide
Zero duplication. Schemas are defined once with both validation rules and OpenAPI metadata:
export const UserSchema = z.object({
email: z.string().email().openapi({
description: 'User email',
example: 'john@example.com'
})
}).openapi('User');From this single definition, you get:
- β Runtime validation (backend) - via AJV + OpenAPI
- β TypeScript types (frontend & backend)
- β OpenAPI documentation (automatic)
π See ARCHITECTURE.md for detailed explanation.
π See OPENAPI_VALIDATION.md for AJV-based validation details.
This project implements a shared type system where:
- Zod schemas with OpenAPI metadata are defined in the
shared/package - TypeScript types are automatically inferred from Zod schemas
- Backend API uses schemas for validation
- Frontend uses inferred types for type safety
- OpenAPI spec is generated directly from schemas (no duplication!)
shared/ # Standalone shared package
βββ src/
β βββ schemas/ # Zod schemas (single source of truth)
β β βββ user.schema.ts
β β βββ index.ts
β βββ index.ts
βββ dist/ # Built types (generated)
βββ package.json
βββ tsconfig.json
src/
βββ lib/
β βββ server/ # Server-only code
β βββ db.ts # In-memory database
βββ routes/
β βββ api/ # API routes
β β βββ users/
β β βββ +server.ts # GET /api/users, POST /api/users
β β βββ [id]/+server.ts # GET/PUT/DELETE /api/users/:id
β βββ +page.svelte # Frontend UI
scripts/
βββ types.ts # OpenAPI metadata types
βββ route-scanner.ts # Automatic route discovery
βββ generate-openapi.ts # OpenAPI spec generator (convention-based)
βββ README.md # Script documentation
static/
βββ openapi.json # Generated OpenAPI spec (auto-generated)
All types are defined once in a separate shared/ package using Zod schemas:
// shared/src/schemas/user.schema.ts
export const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
email: z.string().email(),
createdAt: z.string().datetime()
});
export type User = z.infer<typeof UserSchema>;The shared package has its own build process and generates TypeScript declarations that both frontend and backend can use.
API routes are automatically validated using AJV against the generated OpenAPI spec:
// src/hooks.server.ts - Validates ALL API requests before route handlers
export const handle: Handle = async ({ event, resolve }) => {
if (event.url.pathname.startsWith('/api/')) {
const validationResult = validateRequest(method, pathname, { body, params, query });
if (!validationResult.valid) {
return json({ error: 'Validation failed', details: validationResult.errors }, { status: 400 });
}
}
return resolve(event);
};
// src/routes/api/users/+server.ts - Clean handlers, no manual validation!
export const POST: RequestHandler = async ({ request }) => {
const body = await request.json() as CreateUser; // Already validated by hook!
// Just handle business logic...
};Validation happens automatically at the hook level using AJV + OpenAPI spec!
Frontend components import and use the shared types from the standalone package:
// src/routes/+page.svelte
import type { User, CreateUser } from '@zod-schema-example/shared';
let users = $state<User[]>([]);
let formData = $state<CreateUser>({ name: '', email: '' });The OpenAPI spec is automatically generated from your SvelteKit route structure:
// Just add metadata to your route files:
export const _openapi: RouteOpenAPIMetadata = {
operations: [
{
method: 'GET',
summary: 'List all users',
tags: ['Users'],
responses: [
{ status: 200, description: 'List of users', schema: z.array(UserSchema) }
]
}
]
};Run npm run generate:openapi and it automatically:
- π Scans all
+server.tsfiles - πΊοΈ Converts SvelteKit paths (
[id]) to OpenAPI paths ({id}) - π Generates complete OpenAPI spec
Access your API documentation:
- Swagger UI:
http://localhost:5173/api-docs(interactive) - JSON Spec:
http://localhost:5173/api/openapi.json(raw)
π See scripts/README.md for details on the automatic generation system.
npm install
cd shared && npm install && cd ..Before running the dev server for the first time:
# 1. Build the shared package (generates TypeScript types)
npm run build:shared
# 2. Generate OpenAPI spec (required for request validation)
npm run generate:openapi
# 3. Start dev server
npm run devThe app will be available at:
- Frontend:
http://localhost:5173 - API Docs (Swagger UI):
http://localhost:5173/api-docs - OpenAPI Spec:
http://localhost:5173/api/openapi.json
When you change schemas in shared/src/schemas/:
npm run build:shared # Rebuild types
npm run generate:openapi # Regenerate OpenAPI spec
# Dev server will auto-reload!When you change frontend/backend code (not schemas):
- Just save your files - hot reload works automatically!
π See WORKFLOW.md for detailed development workflow.
npm run buildThis will:
- Build the shared package (compile TypeScript, generate type declarations)
- Generate the OpenAPI spec from the schemas
- Build the SvelteKit application
# Build shared package (when schemas change)
npm run build:shared
# Generate OpenAPI spec (when schemas change)
npm run generate:openapiNote: npm run dev does NOT automatically rebuild these. Run them manually when you change schemas.
npm run generate:openapiThe OpenAPI spec will be saved to static/openapi.json
GET /api/users- Get all usersPOST /api/users- Create a new userGET /api/users/:id- Get a user by IDPUT /api/users/:id- Update a userDELETE /api/users/:id- Delete a user
Adding a new endpoint is simple with convention-based routing:
- Create a route file following SvelteKit conventions:
# For /api/products
src/routes/api/products/+server.ts
# For /api/products/{id}
src/routes/api/products/[id]/+server.ts- Define your schema in
shared/src/schemas/:
// shared/src/schemas/product.schema.ts
import { z } from 'zod';
import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
extendZodWithOpenApi(z);
export const ProductSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
price: z.number().positive()
}).openapi('Product');
export type Product = z.infer<typeof ProductSchema>;- Add OpenAPI metadata to your route file:
// src/routes/api/products/+server.ts
import type { RouteOpenAPIMetadata } from '../../../scripts/types.js';
import { ProductSchema, CreateProductSchema } from '@zod-schema-example/shared';
export const _openapi: RouteOpenAPIMetadata = {
operations: [
{
method: 'GET',
summary: 'List products',
tags: ['Products'],
responses: [
{ status: 200, description: 'List of products', schema: z.array(ProductSchema) }
]
},
{
method: 'POST',
summary: 'Create product',
tags: ['Products'],
requestBody: { schema: CreateProductSchema },
responses: [
{ status: 201, description: 'Product created', schema: ProductSchema },
{ status: 400, description: 'Invalid input' }
]
}
]
};
export const GET: RequestHandler = async () => { /* ... */ };
export const POST: RequestHandler = async () => { /* ... */ };- Rebuild and regenerate:
npm run build:shared # Build the new schema
npm run generate:openapi # Auto-discovers your new route!That's it! Your new endpoint is automatically added to the OpenAPI spec. No manual registration needed.
β
Single Source of Truth: Types are defined once in Zod schemas
β
Convention-Based: API structure follows SvelteKit file structure
β
Automatic Discovery: New routes are automatically found and documented
β
OpenAPI Validation: Requests validated at middleware level (like express-openapi-validator)
β
Clean Route Handlers: No validation boilerplate in endpoints
β
Type Safety: TypeScript types are automatically inferred
β
API Documentation: OpenAPI spec is auto-generated from routes
β
DRY Principle: No duplication between frontend, backend, and docs
β
Refactoring: Changes propagate automatically across the stack
- SvelteKit: Full-stack framework
- TypeScript: Type safety
- Zod: Schema validation and type inference
- @asteasolutions/zod-to-openapi: OpenAPI spec generation
- AJV: JSON schema validator for OpenAPI-based request validation
- ajv-formats: Format validators (email, uuid, date-time, etc.)
- Tailwind CSS: Styling (via @tailwindcss/vite)
1. Define Schema (shared/src/schemas/user.schema.ts)
ββ> Zod schema with OpenAPI metadata
2. Build Shared Package
ββ> npm run build:shared
ββ> Generates TypeScript types
3. Generate OpenAPI Spec
ββ> npm run generate:openapi
ββ> Creates static/openapi.json
4. Start Server
ββ> Hooks load OpenAPI spec
ββ> All /api/* requests validated automatically
5. Frontend Uses Types
ββ> import type { User } from '@zod-schema-example/shared'
6. Backend Uses Types
ββ> const body = await request.json() as CreateUser
ββ> Already validated by OpenAPI middleware!
MIT