Best Practices

Best Practices

This guide covers recommended patterns for building production-ready APIs with SvelteKit Auto OpenAPI.

Route Organization

RESTful Structure

Organize routes following REST conventions:

src/routes/api/
├── users/
│   ├── +server.ts                 # GET /api/users, POST /api/users
│   └── [id]/
│       ├── +server.ts             # GET /api/users/[id], PUT /api/users/[id], DELETE /api/users/[id]
│       └── posts/
│           └── +server.ts         # GET /api/users/[id]/posts
├── posts/
│   ├── +server.ts                 # GET /api/posts, POST /api/posts
│   └── [id]/
│       ├── +server.ts             # GET /api/posts/[id], PUT /api/posts/[id]
│       └── comments/
│           └── +server.ts         # GET /api/posts/[id]/comments
└── auth/
    ├── login/
    │   └── +server.ts             # POST /api/auth/login
    └── logout/
        └── +server.ts             # POST /api/auth/logout
null

Versioning

Version your API routes for backwards compatibility:

src/routes/api/
├── v1/
│   ├── users/
│   │   └── +server.ts
│   └── posts/
│       └── +server.ts
└── v2/
    ├── users/
    │   └── +server.ts
    └── posts/
        └── +server.ts
null

Serve separate docs for each version:

// src/routes/api/v1/docs/[slug]/+server.ts
export const { GET, _config } = ScalarModule({
  openApiOpts: {
    info: { title: 'API v1', version: '1.0.0' }
  }
});

// src/routes/api/v2/docs/[slug]/+server.ts
export const { GET, _config } = ScalarModule({
  openApiOpts: {
    info: { title: 'API v2', version: '2.0.0' }
  }
});
typescript

Group by Feature

For larger APIs, group by feature domain:

src/routes/api/
├── auth/
│   ├── login/+server.ts
│   ├── register/+server.ts
│   └── refresh/+server.ts
├── billing/
│   ├── invoices/+server.ts
│   ├── payment-methods/+server.ts
│   └── subscriptions/+server.ts
└── analytics/
    ├── events/+server.ts
    └── reports/+server.ts
null

Schema Reusability

Shared Schema Library

Create a centralized schema library:

// src/lib/schemas/common.ts
export const IdSchema = {
  type: 'string',
  pattern: '^[0-9]+$'
} as const;

export const TimestampSchema = {
  type: 'string',
  format: 'date-time'
} as const;

export const PaginationSchema = {
  type: 'object',
  properties: {
    page: { type: 'number', minimum: 1, default: 1 },
    limit: { type: 'number', minimum: 1, maximum: 100, default: 20 },
    total: { type: 'number' },
    pages: { type: 'number' }
  }
} as const;

export const ErrorSchema = {
  type: 'object',
  properties: {
    error: { type: 'string' },
    code: { type: 'string' },
    details: { type: 'object' }
  },
  required: ['error', 'code']
} as const;
typescript

Resource Schemas

Define schemas for domain entities:

// src/lib/schemas/user.ts
export const UserSchema = {
  type: 'object',
  properties: {
    id: { type: 'string' },
    email: { type: 'string', format: 'email' },
    name: { type: 'string' },
    createdAt: { type: 'string', format: 'date-time' },
    updatedAt: { type: 'string', format: 'date-time' }
  },
  required: ['id', 'email', 'name']
} as const;

export const CreateUserSchema = {
  type: 'object',
  properties: {
    email: { type: 'string', format: 'email' },
    name: { type: 'string', minLength: 1 },
    password: { type: 'string', minLength: 8 }
  },
  required: ['email', 'name', 'password']
} as const;

export const UpdateUserSchema = {
  type: 'object',
  properties: {
    name: { type: 'string', minLength: 1 },
    email: { type: 'string', format: 'email' }
  }
} as const;
typescript

Composition

Compose complex schemas from simpler ones:

// src/lib/schemas/post.ts
import { UserSchema } from './user';
import { TimestampSchema } from './common';

export const PostSchema = {
  type: 'object',
  properties: {
    id: { type: 'string' },
    title: { type: 'string' },
    content: { type: 'string' },
    author: UserSchema,  // Reuse user schema
    createdAt: TimestampSchema,
    updatedAt: TimestampSchema
  }
} as const;
typescript

With Zod:

import { z } from 'zod';

const TimestampSchema = z.string().datetime();
const UserSchema = z.object({
  id: z.string(),
  email: z.string().email(),
  name: z.string()
});

const PostSchema = z.object({
  id: z.string(),
  title: z.string(),
  content: z.string(),
  author: UserSchema,  // Compose
  createdAt: TimestampSchema,
  updatedAt: TimestampSchema
});
typescript

Validation Strategy

What to Validate

Always validate:

  • User input (body, query, headers)
  • External data (API responses, webhooks)
  • Path parameters
  • Authentication tokens

Consider skipping:

  • Internal service-to-service calls
  • Responses in production (performance)
  • Trusted middleware data

Where to Validate

// vite.config.ts - Global defaults
svelteOpenApi({
  skipValidationDefault: {
    request: {
      headers: false,     // Validate headers (auth tokens)
      query: false,       // Validate query params
      pathParams: false,  // Validate path params
      body: false,        // Validate body
      cookies: true       // Skip cookies (middleware handles)
    },
    response: true  // Skip in production
  }
})
typescript

Validation Depth

Balance thoroughness with performance:

// ✅ Good - validate what matters
const CreateUserSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8).max(100),
  name: z.string().min(1).max(255)
});

// ❌ Too strict - unnecessary constraints
const OverValidatedSchema = z.object({
  email: z.string().email().regex(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/),
  password: z.string().min(8).max(100).regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/),
  name: z.string().min(1).max(255).regex(/^[a-zA-Z\s]+$/)
});
typescript

Documentation Quality

Write Clear Summaries

Use verb + noun format:

POST: {
  summary: 'Create user',           // ✅ Clear action
  description: 'Creates a new user account with email and password',
}

// Not: "User creation endpoint"  ❌ Vague
// Not: "Creates a user"          ❌ Redundant with summary
typescript

Add Descriptions

Provide context and usage details:

POST: {
  summary: 'Create user',
  description: `
Creates a new user account.

The email must be unique. Password must be at least 8 characters.

A verification email will be sent to the provided address.

Returns the created user with an auto-generated ID.
  `,
  requestBody: {
    description: 'User registration data',
    // ...
  }
}
typescript

Use Tags

Organize endpoints with tags:

// In ScalarModule
openApiOpts: {
  tags: [
    { name: 'users', description: 'User management endpoints' },
    { name: 'auth', description: 'Authentication endpoints' },
    { name: 'posts', description: 'Blog post endpoints' }
  ]
}

// In routes
POST: {
  tags: ['users', 'auth'],
  summary: 'Register user',
  // ...
}
typescript

Document Examples

Provide example values:

schema: {
  type: 'object',
  properties: {
    email: {
      type: 'string',
      format: 'email',
      example: 'user@example.com'
    },
    age: {
      type: 'number',
      minimum: 18,
      example: 25
    }
  }
}
typescript

With Zod:

z.object({
  email: z.string().email().describe('User email address').openapi({ example: 'user@example.com' }),
  age: z.number().min(18).describe('User age').openapi({ example: 25 })
})
typescript

Performance Considerations

Skip Response Validation in Production

Response validation has overhead:

const isProd = import.meta.env.PROD;

svelteOpenApi({
  skipValidationDefault: {
    response: isProd  // Skip in production only
  }
})
typescript

Use Efficient Schemas

Avoid overly complex validation:

// ✅ Simple and fast
schema: {
  type: 'object',
  properties: {
    id: { type: 'string' },
    name: { type: 'string' }
  }
}

// ❌ Complex and slow
schema: {
  type: 'object',
  properties: {
    id: { type: 'string', pattern: '^[a-f0-9]{24}$' },
    name: {
      type: 'string',
      pattern: '^[A-Z][a-zA-Z]{1,50}$',
      minLength: 2,
      maxLength: 50
    },
    tags: {
      type: 'array',
      items: { type: 'string', pattern: '^[a-z-]+$' },
      uniqueItems: true
    }
  }
}
typescript

Lazy Load Schemas

For rarely-used endpoints, consider lazy loading:

// Only import when needed
export async function POST({ request }) {
  const { PostSchema } = await import('$lib/schemas/post');
  // Validate and process
}
typescript

Cache Validation Results

For repeated validations:

const schemaCache = new Map();

function getValidator(schema) {
  const key = JSON.stringify(schema);
  if (!schemaCache.has(key)) {
    schemaCache.set(key, new Validator(schema));
  }
  return schemaCache.get(key);
}
typescript

Security

Validate All User Input

Never trust client data:

// ✅ Validate everything
export const _config = {
  openapiOverride: {
    POST: {
      requestBody: {
        $headers: { schema: HeadersSchema },
        $query: { schema: QuerySchema },
        $pathParams: { schema: ParamsSchema },
        content: { 'application/json': { schema: BodySchema } }
      }
    }
  }
} satisfies RouteConfig;
typescript

Sanitize Output

Don't expose sensitive data:

// ❌ Exposes password hash
return json({
  id: user.id,
  email: user.email,
  passwordHash: user.passwordHash  // Don't do this!
});

// ✅ Only public fields
return json({
  id: user.id,
  email: user.email,
  name: user.name
});
typescript

Rate Limiting

Add rate limiting to protect endpoints:

// src/hooks.server.ts
import { rateLimit } from '$lib/rateLimit';

export async function handle({ event, resolve }) {
  if (event.url.pathname.startsWith('/api/')) {
    const limited = await rateLimit(event.getClientAddress());
    if (limited) {
      return new Response('Too many requests', { status: 429 });
    }
  }

  return resolve(event);
}
typescript

Validate File Uploads

For file upload endpoints:

export async function POST({ request, error }) {
  const formData = await request.formData();
  const file = formData.get('file');

  if (!(file instanceof File)) {
    error(400, { error: 'File required' });
  }

  // Validate file type
  const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
  if (!allowedTypes.includes(file.type)) {
    error(400, { error: 'Invalid file type' });
  }

  // Validate file size (5MB max)
  if (file.size > 5 * 1024 * 1024) {
    error(400, { error: 'File too large' });
  }

  // Process file...
}
typescript

Use HTTPS

Always use HTTPS in production:

// In ScalarModule
openApiOpts: {
  servers: [
    {
      url: 'https://api.example.com',  // ✅ HTTPS
      description: 'Production'
    }
  ]
}
typescript

Testing

Test Validation

Write tests for schema validation:

import { describe, test, expect } from 'bun:test';
import { POST } from './+server';

describe('POST /api/users', () => {
  test('accepts valid input', async () => {
    const event = createEvent({
      body: { email: 'test@example.com', password: 'secure123' }
    });

    const response = await POST(event);
    expect(response.status).toBe(201);
  });

  test('rejects invalid email', async () => {
    const event = createEvent({
      body: { email: 'invalid', password: 'secure123' }
    });

    const response = await POST(event);
    expect(response.status).toBe(400);
  });

  test('rejects short password', async () => {
    const event = createEvent({
      body: { email: 'test@example.com', password: 'short' }
    });

    const response = await POST(event);
    expect(response.status).toBe(400);
  });
});
typescript

Test OpenAPI Schema

Validate generated OpenAPI schema:

import { describe, test, expect } from 'bun:test';
import schemaPaths from 'virtual:sveltekit-auto-openapi/schema-paths';

describe('OpenAPI Schema', () => {
  test('includes all endpoints', () => {
    expect(schemaPaths['/api/users']).toBeDefined();
    expect(schemaPaths['/api/users'].POST).toBeDefined();
  });

  test('has required properties', () => {
    const operation = schemaPaths['/api/users'].POST;
    expect(operation.summary).toBeDefined();
    expect(operation.requestBody).toBeDefined();
    expect(operation.responses['201']).toBeDefined();
  });
});
typescript

Environment Configuration

Development vs Production

Use different settings per environment:

// vite.config.ts
const isDev = process.env.NODE_ENV === 'development';

export default defineConfig({
  plugins: [
    sveltekit(),
    svelteOpenApi({
      showDebugLogs: isDev,
      returnsDetailedErrorDefault: {
        request: isDev,  // Detailed in dev
        response: false
      },
      skipValidationDefault: {
        response: !isDev  // Skip response validation in prod
      }
    })
  ]
});
typescript

Environment Variables

Use env vars for configuration:

// .env
API_RATE_LIMIT=100
API_VERSION=v1

// .env.production
API_RATE_LIMIT=1000
API_VERSION=v2
typescript

Access in code:

const rateLimit = parseInt(import.meta.env.API_RATE_LIMIT || '100');
const version = import.meta.env.API_VERSION || 'v1';
typescript

Monitoring

Log Validation Failures

Track validation errors:

export async function POST({ validated, json, error }) {
  try {
    const result = await createUser(validated.body);
    return json(result);
  } catch (err) {
    // Log for monitoring
    console.error('User creation failed:', {
      error: err.message,
      input: validated.body,
      timestamp: new Date().toISOString()
    });

    error(500, { error: 'Internal server error' });
  }
}
typescript

Track Response Times

Monitor performance:

export async function GET({ json }) {
  const start = Date.now();

  try {
    const users = await fetchUsers();
    return json(users);
  } finally {
    const duration = Date.now() - start;
    console.log(`GET /api/users took ${duration}ms`);
  }
}
typescript

Related Documentation