Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

2 Commits

Folders and files

Repository files navigation

SvelteKit CRUD with Shared Zod Types

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

Key Innovation

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.

Architecture

This project implements a shared type system where:

  1. Zod schemas with OpenAPI metadata are defined in the shared/ package
  2. TypeScript types are automatically inferred from Zod schemas
  3. Backend API uses schemas for validation
  4. Frontend uses inferred types for type safety
  5. OpenAPI spec is generated directly from schemas (no duplication!)

Project Structure

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)

Key Features

1. Standalone Shared Package

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.

2. Backend API with AJV Validation

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!

3. Type-Safe Frontend

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: '' });

4. Automatic OpenAPI Generation (Convention-Based)

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.ts files
  • πŸ—ΊοΈ 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.

Getting Started

Install Dependencies

npm install
cd shared && npm install && cd ..

First Time Setup

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 dev

The 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

Development Workflow

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.

Production Build

npm run build

This will:

  1. Build the shared package (compile TypeScript, generate type declarations)
  2. Generate the OpenAPI spec from the schemas
  3. Build the SvelteKit application

Manual Steps (Development)

# Build shared package (when schemas change)
npm run build:shared

# Generate OpenAPI spec (when schemas change)
npm run generate:openapi

Note: npm run dev does NOT automatically rebuild these. Run them manually when you change schemas.

Generate OpenAPI Spec

npm run generate:openapi

The OpenAPI spec will be saved to static/openapi.json

API Endpoints

  • GET /api/users - Get all users
  • POST /api/users - Create a new user
  • GET /api/users/:id - Get a user by ID
  • PUT /api/users/:id - Update a user
  • DELETE /api/users/:id - Delete a user

Adding New API Endpoints

Adding a new endpoint is simple with convention-based routing:

  1. 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
  1. 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>;
  1. 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 () => { /* ... */ };
  1. 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.

Benefits of This Architecture

βœ… 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

Technologies Used

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

Complete Flow

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!

License

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages