| name | api-designer |
| description | Type-safe API design: Zod validation, Result types, SvelteKit endpoints, middleware patterns. |
| when_to_use | When designing or reviewing an API endpoint, request/response contract, or validation layer โ auto-loads on files under routes/ or api/, or when the conversation turns to API design, Zod schemas, or error handling. |
| user-invocable | false |
| effort | medium |
| paths | ["**/routes/**","**/api/**"] |
| allowed-tools | ["Read","Glob","Grep"] |
TypeScript API Design
Comprehensive guide to designing type-safe APIs with TypeScript. Covers type-safe contracts, validation with Zod, Result types for error handling, SvelteKit endpoints, middleware patterns, and API versioning.
When This Skill Applies
Use this skill when:
- Designing API endpoints
- Creating type-safe API contracts
- Implementing validation
- Handling API errors
- Building SvelteKit API routes
- Creating reusable middleware
- Versioning APIs
- Questions about API design patterns
Type-Safe Contracts
Request/Response Types
export interface CreateUserRequest {
email: string;
name: string;
password: string;
}
export interface User {
id: string;
email: string;
name: string;
createdAt: string;
}
export interface ApiError {
code: string;
message: string;
details?: Record<string, string[]>;
}
Key principle: Types ARE documentation. Well-named types with clear structure tell the story.
Result Type Pattern
export type Result<T, E = string> =
| { success: true; data: T }
| { success: false; error: E };
function findUser(id: string): Result<User, 'not_found'> {
const user = db.findUser(id);
if (!user) {
return { success: false, error: 'not_found' };
}
return { success: true, data: user };
}
const result = findUser('123');
if (result.success) {
console.log(result.data.email);
} else {
console.error('Error:', result.error);
}
API Response Format
export interface ApiSuccess<T> {
success: true;
data: T;
}
export interface ApiError {
success: false;
error: {
code: string;
message: string;
details?: Record<string, string[]>;
};
}
export type ApiResponse<T> = ApiSuccess<T> | ApiError;
Endpoint Type Map
export interface ApiEndpoints {
'POST /api/users': {
request: CreateUserRequest;
response: User;
};
'GET /api/users/:id': {
params: { id: string };
response: User;
};
'PATCH /api/users/:id': {
params: { id: string };
request: Partial<UpdateUserRequest>;
response: User;
};
'DELETE /api/users/:id': {
params: { id: string };
response: null;
};
}
Additional resources
Deep-dive detail lives in supporting files, loaded only when needed:
- validation-and-errors.md โ Zod schemas, Result types vs. Error classes, centralised error handling
- sveltekit-and-middleware.md โ GET/POST/PATCH/DELETE endpoint patterns, auth/validation/rate-limit/composed middleware
- auth-and-database.md โ cookie/JWT/Supabase session patterns, typed PostgreSQL and Neo4j wrappers
- versioning-and-clients.md โ URL-based versioning, type-safe API clients, OpenAPI generation, endpoint/business-logic testing
- privacy.md โ response filtering, request minimisation, sensitive-data handling, auth scoping, data deletion
Best Practices
1. Always Validate Input
const data = CreateUserSchema.parse(input);
const user = await createUser(data);
const user = await createUser(input);
2. Use Result Types for Expected Failures
function findUser(id: string): Result<User, 'not_found'> {
const user = db.findUser(id);
if (!user) {
return { success: false, error: 'not_found' };
}
return { success: true, data: user };
}
function findUser(id: string): User {
const user = db.findUser(id);
if (!user) throw new Error('Not found');
return user;
}
3. Throw Error Classes for Unexpected Failures
async function connectDatabase(): Promise<Database> {
try {
return await connect();
} catch (error) {
throw new DatabaseError('Failed to connect');
}
}
async function connectDatabase(): Result<Database, string> {
}
4. Type Everything Explicitly
interface CreateUserRequest {
email: string;
name: string;
}
async function createUser(data: CreateUserRequest): Promise<User> {
}
async function createUser(data): Promise<any> {
}
5. Separate Concerns (Layered Architecture)
export const POST: RequestHandler = async ({ request }) => {
const data = await validateRequest(request, CreateUserSchema);
const user = await createUser(data);
return json({ success: true, data: user }, { status: 201 });
};
async function createUser(data: CreateUserRequest): Promise<User> {
const passwordHash = await hashPassword(data.password);
return await db.users.create({ ...data, passwordHash });
}
export const POST: RequestHandler = async ({ request }) => {
const body = await request.json();
};
6. Use Middleware for Cross-Cutting Concerns
export const POST: RequestHandler = async (event) => {
const user = await requireAuth(event);
const data = await validateRequest(event.request, CreatePostSchema);
const post = await createPost(user.id, data);
return json({ success: true, data: post });
};
export const POST: RequestHandler = async ({ request, cookies }) => {
const session = cookies.get('session');
if (!session) throw error(401);
const user = await verifySession(session);
const body = await request.json();
if (!body.title) throw error(400);
};
7. Document with Types, Not Comments
interface CreateUserRequest {
email: string;
name: string;
password: string;
}
interface CreateUserRequest {
email: string;
name: string;
password: string;
}
Success Criteria
API design is well-structured when:
- All requests/responses explicitly typed
- Input validated with Zod schemas
- Expected failures use Result types
- Unexpected failures throw Error classes
- Endpoints follow REST conventions
- Middleware reusable and composable
- Database layer has typed wrappers
- Authentication consistent across endpoints
- Errors have standardised format
- Type-safe client available for consumption
- Tests cover happy path and error cases
- Code is maintainable and follows patterns
- Responses expose minimum necessary data (privacy-by-default)
- No sensitive data logged or leaked
- Auth scoping prevents cross-user data access
- Data deletion is supported and complete