diff --git a/.changeset/README.md b/.changeset/README.md
index e5b6d8d..d09343f 100644
--- a/.changeset/README.md
+++ b/.changeset/README.md
@@ -6,3 +6,96 @@ find the full documentation for it [in our repository](https://github.com/change
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
+
+## Creating a Changeset
+
+When you make changes that should be included in a release, create a changeset to document them:
+
+### From the root directory:
+
+```bash
+pnpm changeset
+```
+
+This will:
+
+1. Prompt you to select which packages have changed
+2. Ask you to choose the type of change (major, minor, patch)
+3. Request a summary of the changes
+
+### Changeset Types
+
+- **major** - Breaking changes (e.g., API changes, removed features)
+- **minor** - New features (e.g., new functionality, new APIs)
+- **patch** - Bug fixes and small improvements
+
+### Example Workflow
+
+```bash
+# 1. Make your changes to the codebase
+# 2. Create a changeset
+pnpm changeset
+
+# 3. Select packages (use space to select, enter to confirm):
+# - @ts-contract/core (if you changed core)
+# - @ts-contract/plugins (if you changed plugins)
+
+# 4. Choose version bump type:
+# - patch (0.0.x) for bug fixes
+# - minor (0.x.0) for new features
+# - major (x.0.0) for breaking changes
+
+# 5. Write a summary describing your changes
+
+# 6. Commit the changeset file along with your changes
+git add .changeset/your-changeset-name.md
+git commit -m "feat: add new feature"
+```
+
+### Manual Changeset Creation
+
+You can also create changeset files manually in `.changeset/`:
+
+```markdown
+---
+'@ts-contract/core': minor
+'@ts-contract/plugins': minor
+---
+
+Add WebSocket contract support with bidirectional message schemas
+```
+
+### Release Process (Automated)
+
+Releases are **fully automated** via GitHub Actions:
+
+1. **Merge PR to main** - When your PR with a changeset is merged to `main`
+2. **Changesets bot creates a "Version Packages" PR** - This PR updates package versions and CHANGELOGs
+3. **Merge the "Version Packages" PR** - This triggers:
+ - Automatic publishing to npm
+ - GitHub releases creation
+ - Documentation deployment to Cloudflare Pages
+
+**You don't need to run any manual commands!** The CI pipeline handles everything.
+
+### Manual Release (If Needed)
+
+Only use these commands if you need to release locally (rare):
+
+```bash
+# Update package versions based on changesets
+pnpm changeset version
+
+# Build packages
+pnpm -r build
+
+# Publish to npm (requires npm authentication)
+pnpm changeset publish
+```
+
+### Best Practices
+
+1. **Create changesets with your PR** - Don't wait until release time
+2. **Be descriptive** - Write clear summaries that will appear in CHANGELOGs
+3. **One changeset per logical change** - Multiple changesets are fine for complex PRs
+4. **Include all affected packages** - If a change affects multiple packages, select them all
diff --git a/.changeset/config.json b/.changeset/config.json
index 4f9f447..dc8884a 100644
--- a/.changeset/config.json
+++ b/.changeset/config.json
@@ -7,5 +7,14 @@
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
- "ignore": ["@ts-contract/docs"]
+ "ignore": [
+ "@ts-contract/docs",
+ "@ts-contract-recipes/client",
+ "@ts-contract-recipes/server",
+ "@ts-contract-recipes/shared",
+ "@ts-contract-recipes/fastify-react-query",
+ "@ts-contract-recipes/hono-custom-fetch",
+ "@ts-contract-recipes/custom-plugins",
+ "@ts-contract-recipes/express"
+ ]
}
diff --git a/.changeset/pre.json b/.changeset/pre.json
index 6460fed..1fd6f9a 100644
--- a/.changeset/pre.json
+++ b/.changeset/pre.json
@@ -4,9 +4,17 @@
"initialVersions": {
"@ts-contract/docs": "0.0.0",
"@ts-contract/core": "0.0.1",
- "@ts-contract/plugins": "0.0.1"
+ "@ts-contract/plugins": "0.0.1",
+ "@ts-contract-recipes/custom-plugins": "1.0.0",
+ "@ts-contract-recipes/express": "1.0.0",
+ "@ts-contract-recipes/fastify-react-query": "1.0.0",
+ "@ts-contract-recipes/client": "1.0.0",
+ "@ts-contract-recipes/server": "1.0.0",
+ "@ts-contract-recipes/shared": "1.0.0",
+ "@ts-contract-recipes/hono-custom-fetch": "1.0.0"
},
"changesets": [
+ "sparkly-pianos-brake",
"upset-eyes-brush"
]
}
diff --git a/.changeset/sparkly-pianos-brake.md b/.changeset/sparkly-pianos-brake.md
new file mode 100644
index 0000000..bd2f492
--- /dev/null
+++ b/.changeset/sparkly-pianos-brake.md
@@ -0,0 +1,6 @@
+---
+'@ts-contract/plugins': minor
+'@ts-contract/core': minor
+---
+
+Add experimental websocket contract
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..2269984
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,3 @@
+# These are supported funding model platforms
+
+github: mbrimmer83
diff --git a/README.md b/README.md
index 7c3a66b..e8c0a75 100644
--- a/README.md
+++ b/README.md
@@ -54,6 +54,61 @@ const user = api.getUser.validateResponse(200, data);
// => { id: string, name: string, email: string }
```
+### WebSocket Example
+
+```ts
+import { createContract, initContract } from '@ts-contract/core';
+import {
+ websocketPathPlugin,
+ websocketValidatePlugin,
+} from '@ts-contract/plugins';
+import { z } from 'zod';
+
+const contract = createContract({
+ chat: {
+ type: 'websocket',
+ path: '/ws/chat/:roomId',
+ pathParams: z.object({ roomId: z.string() }),
+ query: z.object({ token: z.string() }),
+ clientMessages: {
+ new_msg: z.object({
+ type: z.literal('new_msg'),
+ body: z.string(),
+ }),
+ },
+ serverMessages: {
+ new_msg: z.object({
+ type: z.literal('new_msg'),
+ id: z.string(),
+ body: z.string(),
+ userId: z.string(),
+ }),
+ },
+ },
+});
+
+const api = initContract(contract)
+ .useWebSocket(websocketPathPlugin)
+ .useWebSocket(websocketValidatePlugin)
+ .build();
+
+// Build WebSocket URL
+const url = api.chat.buildPath({ roomId: '123' }, { token: 'abc' });
+// => "/ws/chat/123?token=abc"
+
+// Validate outgoing message
+const msg = api.chat.validateClientMessage('new_msg', {
+ type: 'new_msg',
+ body: 'Hello!',
+});
+
+// Validate incoming message (e.g., with Phoenix.js)
+channel.on('new_msg', (data) => {
+ const validated = api.chat.validateServerMessage('new_msg', data);
+ console.log(validated.body);
+});
+```
+
## Packages
| Package | Description |
diff --git a/apps/docs/components/construction-banner.tsx b/apps/docs/components/construction-banner.tsx
index e738f03..083de76 100644
--- a/apps/docs/components/construction-banner.tsx
+++ b/apps/docs/components/construction-banner.tsx
@@ -7,7 +7,8 @@ export function ConstructionBanner() {
🚧
- Under Construction - This documentation is actively being developed
+ Alpha Release - This project is in early development and APIs may
+ change
diff --git a/apps/docs/content/api-reference/core/create-contract.mdx b/apps/docs/content/api-reference/core/create-contract.mdx
new file mode 100644
index 0000000..001e8bb
--- /dev/null
+++ b/apps/docs/content/api-reference/core/create-contract.mdx
@@ -0,0 +1,268 @@
+---
+title: createContract
+description: API reference for the createContract function.
+---
+
+## Overview
+
+`createContract` is the primary function for defining your API contract. It accepts a contract definition and returns it with full type inference.
+
+## Import
+
+```ts
+import { createContract } from '@ts-contract/core';
+```
+
+## Signature
+
+```ts
+function createContract(contract: C): C
+```
+
+### Type Parameters
+
+- **C** - The contract definition type, inferred from the argument
+
+### Parameters
+
+- **contract** - An object defining your API routes and nested contracts
+
+### Returns
+
+The same contract object with full TypeScript type information
+
+## Usage
+
+### Basic Contract
+
+```ts
+import { createContract } from '@ts-contract/core';
+import { z } from 'zod';
+
+const contract = createContract({
+ getUser: {
+ method: 'GET',
+ path: '/users/:id',
+ pathParams: z.object({ id: z.string() }),
+ responses: {
+ 200: z.object({
+ id: z.string(),
+ name: z.string(),
+ email: z.string().email(),
+ }),
+ 404: z.object({ message: z.string() }),
+ },
+ },
+});
+```
+
+
+
+
+## Related Types
+
+### ContractDef
+
+The type for a contract definition:
+
+```ts
+interface ContractDef {
+ [key: string]: RouteDef | ContractDef;
+}
+```
+
+A contract is an object where each value is either:
+- A `RouteDef` (route definition)
+- Another `ContractDef` (nested contract)
+
+### RouteDef
+
+The type for a route definition:
+
+```ts
+type RouteDef = {
+ method: Method;
+ path: string;
+ pathParams?: SchemaProtocol;
+ query?: SchemaProtocol;
+ headers?: Record>;
+ body?: SchemaProtocol;
+ responses: Partial>>;
+ summary?: string;
+ metadata?: Record;
+};
+```
+
+**Required fields:**
+- `method` - HTTP method (GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD)
+- `path` - URL path template with optional `:param` placeholders
+- `responses` - Object mapping status codes to response schemas
+
+**Optional fields:**
+- `pathParams` - Schema for path parameters
+- `query` - Schema for query string parameters
+- `headers` - Object mapping header names to schemas
+- `body` - Schema for request body
+- `summary` - Human-readable description
+- `metadata` - Custom key-value pairs
+
+### Method
+
+```ts
+type Method = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS' | 'HEAD';
+```
+
+### HttpStatusCodes
+
+```ts
+type HttpStatusCodes =
+ | 100 | 101 | 102
+ | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226
+ | 300 | 301 | 302 | 303 | 304 | 305 | 307 | 308
+ | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409
+ | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419
+ | 420 | 421 | 422 | 423 | 424 | 428 | 429 | 431 | 451
+ | 500 | 501 | 502 | 503 | 504 | 505 | 507 | 511;
+```
+
+
+## Examples
+
+### With Metadata
+
+```ts
+const contract = createContract({
+ getUser: {
+ method: 'GET',
+ path: '/users/:id',
+ pathParams: z.object({ id: z.string() }),
+ responses: {
+ 200: z.object({ id: z.string(), name: z.string() }),
+ },
+ summary: 'Retrieve a user by ID',
+ metadata: {
+ requiresAuth: true,
+ rateLimit: 100,
+ version: 'v1',
+ },
+ },
+});
+```
+
+### With All Fields
+
+```ts
+const contract = createContract({
+ updateUser: {
+ method: 'PUT',
+ path: '/users/:id',
+ pathParams: z.object({ id: z.string() }),
+ query: z.object({ notify: z.boolean().optional() }),
+ headers: {
+ 'authorization': z.string(),
+ 'x-api-version': z.string(),
+ },
+ body: z.object({
+ name: z.string(),
+ email: z.string().email(),
+ }),
+ responses: {
+ 200: z.object({
+ id: z.string(),
+ name: z.string(),
+ email: z.string(),
+ updatedAt: z.string(),
+ }),
+ 400: z.object({ message: z.string() }),
+ 401: z.object({ message: z.string() }),
+ 404: z.object({ message: z.string() }),
+ },
+ summary: 'Update a user profile',
+ metadata: {
+ requiresAuth: true,
+ },
+ },
+});
+```
+
+## Best Practices
+
+### Export Contracts
+
+Export your contract for use throughout your application:
+
+```ts
+// contract.ts
+export const contract = createContract({
+ getUser: { /* ... */ },
+ createUser: { /* ... */ },
+});
+```
+
+```ts
+// Other files
+import { contract } from './contract';
+```
+
+### Use Shared Schemas
+
+Extract common schemas to avoid duplication:
+
+```ts
+const UserSchema = z.object({
+ id: z.string(),
+ name: z.string(),
+ email: z.string().email(),
+});
+
+const ErrorSchema = z.object({ message: z.string() });
+
+const contract = createContract({
+ getUser: {
+ method: 'GET',
+ path: '/users/:id',
+ pathParams: z.object({ id: z.string() }),
+ responses: {
+ 200: UserSchema,
+ 404: ErrorSchema,
+ },
+ },
+ createUser: {
+ method: 'POST',
+ path: '/users',
+ body: UserSchema.omit({ id: true }),
+ responses: {
+ 201: UserSchema,
+ 400: ErrorSchema,
+ },
+ },
+});
+```
+
+### Organize by Resource
+
+Group related routes together:
+
+```ts
+const contract = createContract({
+ users: {
+ list: { /* ... */ },
+ get: { /* ... */ },
+ create: { /* ... */ },
+ update: { /* ... */ },
+ delete: { /* ... */ },
+ },
+ posts: {
+ list: { /* ... */ },
+ get: { /* ... */ },
+ create: { /* ... */ },
+ },
+});
+```
+
+## See Also
+
+- [initContract](/api-reference/core/init-contract) - Initialize a contract with plugins
+- [Type Helpers](/api-reference/core/type-helpers) - Extract types from contracts
+- [Contracts Guide](/core-concepts/contracts) - Learn about contract organization
+- [Routes & Schemas](/core-concepts/routes-and-schemas) - Route definition details
diff --git a/apps/docs/content/api-reference/core/init-contract.mdx b/apps/docs/content/api-reference/core/init-contract.mdx
new file mode 100644
index 0000000..9338cf9
--- /dev/null
+++ b/apps/docs/content/api-reference/core/init-contract.mdx
@@ -0,0 +1,301 @@
+---
+title: initContract
+description: API reference for the initContract function and ContractBuilder.
+---
+
+## Overview
+
+`initContract` creates a builder that allows you to compose plugins onto your contract. It returns a `ContractBuilder` instance with `.use()` and `.build()` methods.
+
+## Import
+
+```ts
+import { initContract } from '@ts-contract/core';
+```
+
+## Signature
+
+```ts
+function initContract(
+ contract: C
+): ContractBuilder
+```
+
+### Type Parameters
+
+- **C** - The contract definition type, inferred from the argument
+
+### Parameters
+
+- **contract** - A contract created with `createContract()`
+
+### Returns
+
+A `ContractBuilder` instance with no plugins initially applied
+
+## ContractBuilder API
+
+The builder returned by `initContract` has two methods:
+
+### .use()
+
+Adds a plugin to the builder.
+
+```ts
+use
(plugin: P): ContractBuilder
+```
+
+**Parameters:**
+- **plugin** - A plugin object implementing `ContractPlugin`
+
+**Returns:**
+- A new builder with the plugin added
+
+**Example:**
+
+```ts
+import { pathPlugin, validatePlugin } from '@ts-contract/plugins';
+
+const builder = initContract(contract)
+ .use(pathPlugin)
+ .use(validatePlugin);
+```
+
+### .build()
+
+Produces the final enhanced contract with all plugin methods.
+
+```ts
+build(): ApplyPlugins
+```
+
+**Returns:**
+- The contract with all plugin methods added to each route
+
+**Example:**
+
+```ts
+const api = initContract(contract)
+ .use(pathPlugin)
+ .use(validatePlugin)
+ .build();
+
+// Now routes have plugin methods
+api.getUser.buildPath({ id: '123' });
+api.getUser.validateResponse(200, data);
+```
+
+## Usage
+
+### Basic Usage
+
+```ts
+import { createContract, initContract } from '@ts-contract/core';
+import { pathPlugin, validatePlugin } from '@ts-contract/plugins';
+import { z } from 'zod';
+
+const contract = createContract({
+ getUser: {
+ method: 'GET',
+ path: '/users/:id',
+ pathParams: z.object({ id: z.string() }),
+ responses: {
+ 200: z.object({ id: z.string(), name: z.string() }),
+ },
+ },
+});
+
+const api = initContract(contract)
+ .use(pathPlugin)
+ .use(validatePlugin)
+ .build();
+```
+
+
+
+
+
+
+### Conditional Plugins
+
+Add plugins conditionally based on environment:
+
+```ts
+const builder = initContract(contract).use(pathPlugin);
+
+const api = process.env.NODE_ENV === 'development'
+ ? builder.use(validatePlugin).build()
+ : builder.build();
+```
+
+### Custom Plugin Chain
+
+```ts
+import { customLoggerPlugin } from './plugins/logger';
+import { customCachePlugin } from './plugins/cache';
+import { customMetricsPlugin } from './plugins/metrics';
+
+const api = initContract(contract)
+ .use(pathPlugin)
+ .use(validatePlugin)
+ .use(customLoggerPlugin)
+ .use(customCachePlugin)
+ .use(customMetricsPlugin)
+ .build();
+```
+
+## Related Types
+
+### ContractPlugin
+
+```ts
+interface ContractPlugin {
+ name: Name;
+ route: (route: RouteDef) => Record;
+}
+```
+
+A plugin object with:
+- **name** - Unique identifier for the plugin
+- **route** - Function that receives a route and returns methods to add
+
+### ApplyPlugins
+
+```ts
+type ApplyPlugins = {
+ [K in keyof C]: C[K] extends RouteDef
+ ? MergePluginReturns
+ : C[K] extends ContractDef
+ ? ApplyPlugins
+ : never;
+};
+```
+
+Type that recursively applies plugin methods to all routes in a contract.
+
+### PluginTypeRegistry
+
+```ts
+interface PluginTypeRegistry {
+ // Plugins register their types here via declaration merging
+}
+```
+
+Type registry that plugins use to declare their return types.
+
+## Performance
+
+`initContract` and the builder have minimal overhead:
+
+- **Build time**: Negligible - simple object mapping
+- **Runtime**: No ongoing cost after `.build()`
+- **Memory**: Small - one object per route with plugin methods
+- **Bundle size**: ~1KB for the builder logic
+
+The performance impact comes from the plugins themselves, not from `initContract`.
+
+## Common Patterns
+
+### Shared API Instance
+
+Create one API instance and share it:
+
+```ts
+// api.ts
+export const api = initContract(contract)
+ .use(pathPlugin)
+ .use(validatePlugin)
+ .build();
+
+// Multiple files can import and use it
+import { api } from './api';
+```
+
+### Separate Client and Server APIs
+
+Create different API instances for client and server:
+
+```ts
+// client-api.ts
+export const clientApi = initContract(contract)
+ .use(pathPlugin)
+ .use(validatePlugin)
+ .build();
+
+// server-api.ts
+export const serverApi = initContract(contract)
+ .use(validatePlugin)
+ .build();
+```
+
+### Plugin Factory
+
+Create a factory for consistent plugin configuration:
+
+```ts
+function createApi(contract: C) {
+ return initContract(contract)
+ .use(pathPlugin)
+ .use(validatePlugin)
+ .build();
+}
+
+const userApi = createApi(userContract);
+const postApi = createApi(postContract);
+```
+
+## Troubleshooting
+
+### Plugin Methods Not Available
+
+**Problem:** Plugin methods don't exist on routes.
+
+**Solution:** Make sure you called `.build()`:
+
+```ts
+// ✗ Wrong - forgot .build()
+const api = initContract(contract).use(pathPlugin);
+api.getUser.buildPath({ id: '123' }); // Error!
+
+// ✓ Correct
+const api = initContract(contract).use(pathPlugin).build();
+api.getUser.buildPath({ id: '123' }); // Works!
+```
+
+### Type Errors with Plugins
+
+**Problem:** TypeScript shows errors with plugin methods.
+
+**Solution:** Ensure plugins are properly typed with declaration merging:
+
+```ts
+// Plugin must declare its types
+declare module '@ts-contract/core' {
+ interface PluginTypeRegistry {
+ myPlugin: {
+ myMethod: () => string;
+ };
+ }
+}
+```
+
+### Plugin Order Issues
+
+**Problem:** Plugin methods conflict or override each other.
+
+**Solution:** Check plugin order and ensure plugins don't add methods with the same name:
+
+```ts
+// If plugins conflict, order matters
+const api = initContract(contract)
+ .use(pluginA) // Adds method 'foo'
+ .use(pluginB) // Also adds method 'foo' - will override pluginA's version
+ .build();
+```
+
+## See Also
+
+- [createContract](/api-reference/core/create-contract) - Create a contract
+- [Type Helpers](/api-reference/core/type-helpers) - Extract types from contracts
+- [Plugin System](/core-concepts/plugin-system) - Learn how plugins work
+- [Creating Custom Plugins](/plugins/creating-custom-plugins) - Build your own plugins
diff --git a/apps/docs/content/api-reference/core/meta.json b/apps/docs/content/api-reference/core/meta.json
new file mode 100644
index 0000000..4f3c194
--- /dev/null
+++ b/apps/docs/content/api-reference/core/meta.json
@@ -0,0 +1,4 @@
+{
+ "title": "Core API",
+ "pages": ["create-contract", "init-contract", "type-helpers"]
+}
diff --git a/apps/docs/content/api-reference/core/type-helpers.mdx b/apps/docs/content/api-reference/core/type-helpers.mdx
new file mode 100644
index 0000000..86ce4d9
--- /dev/null
+++ b/apps/docs/content/api-reference/core/type-helpers.mdx
@@ -0,0 +1,591 @@
+---
+title: Type Helpers
+description: Complete API reference for TypeScript type inference helpers.
+---
+
+## Overview
+
+ts-contract provides type helpers that extract TypeScript types from your route definitions. These enable end-to-end type safety without code generation.
+
+All type helpers are compile-time only - they have zero runtime overhead.
+
+## Import
+
+```ts
+import type {
+ InferPathParams,
+ InferQuery,
+ InferBody,
+ InferHeaders,
+ InferResponseBody,
+ InferResponses,
+ InferArgs,
+} from '@ts-contract/core';
+```
+
+## Type Helpers Reference
+
+### InferPathParams
+
+Extract path parameter types from a route.
+
+```ts
+type InferPathParams
+```
+
+**Type Parameters:**
+- **R** - A route definition type
+
+**Returns:**
+- Object type with path parameter properties, or `undefined` if no `pathParams`
+
+**Example:**
+
+```ts
+const contract = createContract({
+ getPost: {
+ method: 'GET',
+ path: '/users/:userId/posts/:postId',
+ pathParams: z.object({
+ userId: z.string(),
+ postId: z.string(),
+ }),
+ responses: {
+ 200: z.object({ id: z.string() }),
+ },
+ },
+});
+
+type Params = InferPathParams;
+// => { userId: string; postId: string }
+```
+
+**Usage:**
+
+```ts
+function handleRequest(params: Params) {
+ console.log(params.userId, params.postId);
+}
+```
+
+---
+
+### InferQuery
+
+Extract query parameter types from a route.
+
+```ts
+type InferQuery
+```
+
+**Type Parameters:**
+- **R** - A route definition type
+
+**Returns:**
+- Object type with query parameter properties, or `undefined` if no `query`
+
+**Example:**
+
+```ts
+const contract = createContract({
+ listUsers: {
+ method: 'GET',
+ path: '/users',
+ query: z.object({
+ page: z.string().optional(),
+ limit: z.string().optional(),
+ sort: z.enum(['asc', 'desc']).optional(),
+ }),
+ responses: {
+ 200: z.array(z.object({ id: z.string() })),
+ },
+ },
+});
+
+type Query = InferQuery;
+// => { page?: string; limit?: string; sort?: 'asc' | 'desc' }
+```
+
+**Usage:**
+
+```ts
+function buildQueryString(query: Query): string {
+ const params = new URLSearchParams();
+ if (query.page) params.set('page', query.page);
+ if (query.limit) params.set('limit', query.limit);
+ if (query.sort) params.set('sort', query.sort);
+ return params.toString();
+}
+```
+
+---
+
+### InferBody
+
+Extract request body type from a route.
+
+```ts
+type InferBody
+```
+
+**Type Parameters:**
+- **R** - A route definition type
+
+**Returns:**
+- Request body type, or `undefined` if no `body`
+
+**Example:**
+
+```ts
+const contract = createContract({
+ createUser: {
+ method: 'POST',
+ path: '/users',
+ body: z.object({
+ name: z.string(),
+ email: z.string().email(),
+ age: z.number().int().min(0),
+ }),
+ responses: {
+ 201: z.object({ id: z.string() }),
+ },
+ },
+});
+
+type Body = InferBody;
+// => { name: string; email: string; age: number }
+```
+
+**Usage:**
+
+```ts
+async function createUser(body: Body) {
+ const response = await fetch('/users', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(body),
+ });
+ return response.json();
+}
+```
+
+---
+
+### InferHeaders
+
+Extract request header types from a route.
+
+```ts
+type InferHeaders
+```
+
+**Type Parameters:**
+- **R** - A route definition type
+
+**Returns:**
+- Object type with header properties, or `undefined` if no `headers`
+
+**Example:**
+
+```ts
+const contract = createContract({
+ getProtected: {
+ method: 'GET',
+ path: '/protected',
+ headers: {
+ 'authorization': z.string(),
+ 'x-api-key': z.string(),
+ },
+ responses: {
+ 200: z.object({ data: z.string() }),
+ },
+ },
+});
+
+type Headers = InferHeaders;
+// => { authorization: string; 'x-api-key': string }
+```
+
+**Usage:**
+
+```ts
+async function fetchProtected(headers: Headers) {
+ const response = await fetch('/protected', { headers });
+ return response.json();
+}
+```
+
+---
+
+### InferResponseBody
+
+Extract a specific response type by status code.
+
+```ts
+type InferResponseBody
+```
+
+**Type Parameters:**
+- **R** - A route definition type
+- **S** - An HTTP status code
+
+**Returns:**
+- Response body type for the specified status code
+
+**Example:**
+
+```ts
+const contract = createContract({
+ getUser: {
+ method: 'GET',
+ path: '/users/:id',
+ pathParams: z.object({ id: z.string() }),
+ responses: {
+ 200: z.object({
+ id: z.string(),
+ name: z.string(),
+ email: z.string(),
+ }),
+ 404: z.object({
+ message: z.string(),
+ }),
+ 500: z.object({
+ message: z.string(),
+ code: z.string(),
+ }),
+ },
+ },
+});
+
+type SuccessResponse = InferResponseBody;
+// => { id: string; name: string; email: string }
+
+type NotFoundResponse = InferResponseBody;
+// => { message: string }
+
+type ErrorResponse = InferResponseBody;
+// => { message: string; code: string }
+```
+
+**Usage:**
+
+```ts
+async function getUser(id: string): Promise {
+ const response = await fetch(`/users/${id}`);
+
+ if (response.status === 404) {
+ const error: NotFoundResponse = await response.json();
+ throw new Error(error.message);
+ }
+
+ if (!response.ok) {
+ throw new Error('Request failed');
+ }
+
+ const user: SuccessResponse = await response.json();
+ return user;
+}
+```
+
+---
+
+### InferResponses
+
+Extract all responses as a discriminated union.
+
+```ts
+type InferResponses
+```
+
+**Type Parameters:**
+- **R** - A route definition type
+
+**Returns:**
+- Discriminated union of `{ status: StatusCode; body: ResponseBody }` for all responses
+
+**Example:**
+
+```ts
+const contract = createContract({
+ getUser: {
+ method: 'GET',
+ path: '/users/:id',
+ pathParams: z.object({ id: z.string() }),
+ responses: {
+ 200: z.object({ id: z.string(), name: z.string() }),
+ 404: z.object({ message: z.string() }),
+ 500: z.object({ message: z.string() }),
+ },
+ },
+});
+
+type Response = InferResponses;
+// =>
+// | { status: 200; body: { id: string; name: string } }
+// | { status: 404; body: { message: string } }
+// | { status: 500; body: { message: string } }
+```
+
+**Usage:**
+
+```ts
+function handleResponse(response: Response) {
+ switch (response.status) {
+ case 200:
+ // response.body is { id: string; name: string }
+ console.log('User:', response.body.name);
+ break;
+ case 404:
+ // response.body is { message: string }
+ console.error('Not found:', response.body.message);
+ break;
+ case 500:
+ // response.body is { message: string }
+ console.error('Server error:', response.body.message);
+ break;
+ }
+}
+```
+
+---
+
+### InferArgs
+
+Merge all input types (params, query, body, headers) into a single object.
+
+```ts
+type InferArgs
+```
+
+**Type Parameters:**
+- **R** - A route definition type
+
+**Returns:**
+- Object with optional `params`, `query`, `body`, and `headers` properties
+
+**Example:**
+
+```ts
+const contract = createContract({
+ updateUser: {
+ method: 'PUT',
+ path: '/users/:id',
+ pathParams: z.object({ id: z.string() }),
+ query: z.object({ notify: z.boolean().optional() }),
+ body: z.object({
+ name: z.string(),
+ email: z.string(),
+ }),
+ responses: {
+ 200: z.object({ id: z.string() }),
+ },
+ },
+});
+
+type Args = InferArgs;
+// => {
+// params: { id: string };
+// query: { notify?: boolean };
+// body: { name: string; email: string };
+// }
+```
+
+**Usage:**
+
+```ts
+function buildUpdateRequest(args: Args): Request {
+ const url = `/users/${args.params.id}${
+ args.query.notify ? '?notify=true' : ''
+ }`;
+
+ return new Request(url, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(args.body),
+ });
+}
+
+// Usage
+const request = buildUpdateRequest({
+ params: { id: '123' },
+ query: { notify: true },
+ body: { name: 'Alice', email: 'alice@example.com' },
+});
+```
+
+
+
+## WebSocket Type Helpers
+
+ts-contract provides type helpers for WebSocket contracts:
+
+### InferWebSocketPathParams
+
+Extract path parameter types from a WebSocket definition:
+
+```ts
+import type { InferWebSocketPathParams } from '@ts-contract/core';
+
+type Params = InferWebSocketPathParams;
+// => { roomId: string }
+```
+
+### InferWebSocketQuery
+
+Extract query parameter types:
+
+```ts
+import type { InferWebSocketQuery } from '@ts-contract/core';
+
+type Query = InferWebSocketQuery;
+// => { token?: string }
+```
+
+### InferWebSocketHeaders
+
+Extract header types:
+
+```ts
+import type { InferWebSocketHeaders } from '@ts-contract/core';
+
+type Headers = InferWebSocketHeaders;
+// => { authorization: string }
+```
+
+### InferClientMessages
+
+Extract all client message types:
+
+```ts
+import type { InferClientMessages } from '@ts-contract/core';
+
+type ClientMsgs = InferClientMessages;
+// => { new_msg: { type: 'new_msg', body: string }, ... }
+```
+
+### InferServerMessages
+
+Extract all server message types:
+
+```ts
+import type { InferServerMessages } from '@ts-contract/core';
+
+type ServerMsgs = InferServerMessages;
+// => { new_msg: { type: 'new_msg', id: string, body: string }, ... }
+```
+
+### InferClientMessage
+
+Extract a specific client message type:
+
+```ts
+import type { InferClientMessage } from '@ts-contract/core';
+
+type NewMsg = InferClientMessage;
+// => { type: 'new_msg', body: string }
+```
+
+### InferServerMessage
+
+Extract a specific server message type:
+
+```ts
+import type { InferServerMessage } from '@ts-contract/core';
+
+type NewMsg = InferServerMessage;
+// => { type: 'new_msg', id: string, body: string }
+```
+
+**Usage:**
+
+```ts
+type Params = InferWebSocketPathParams;
+type ClientMsg = InferClientMessage;
+type ServerMsg = InferServerMessage;
+```
+
+### Template Literal Types
+
+```ts
+type UserId = InferPathParams['id'];
+
+type UserKey = `user:${UserId}`;
+// => "user:string"
+```
+
+### Extracting Nested Types
+
+```ts
+type UserList = InferResponseBody;
+type User = UserList extends Array ? U : never;
+// Extract the array element type
+```
+
+## Best Practices
+
+### 1. Use Type Aliases
+
+Create meaningful type aliases for reuse:
+
+```ts
+// types.ts
+export type User = InferResponseBody;
+export type UserList = InferResponseBody;
+export type CreateUserPayload = InferBody;
+```
+
+### 2. Colocate with Usage
+
+Define types close to where they're used:
+
+```ts
+// user-service.ts
+import type { InferResponseBody } from '@ts-contract/core';
+import { contract } from './contract';
+
+type User = InferResponseBody;
+
+export class UserService {
+ async getUser(id: string): Promise {
+ // ...
+ }
+}
+```
+
+### 3. Use `typeof` Correctly
+
+Always use `typeof` when referencing contract routes:
+
+```ts
+// ✓ Correct
+type User = InferResponseBody;
+
+// ✗ Wrong
+type User = InferResponseBody;
+```
+
+### 4. Avoid `any`
+
+Don't cast to `any` - use proper type inference:
+
+```ts
+// ✗ Avoid
+const user = await response.json() as any;
+
+// ✓ Better
+type User = InferResponseBody;
+const user = await response.json() as User;
+```
+
+## See Also
+
+- [Type Inference Guide](/core-concepts/type-inference) - Learn about type inference
+- [createContract](/api-reference/core/create-contract) - Create a contract
+- [Routes & Schemas](/core-concepts/routes-and-schemas) - Route definition details
+
+## Next Steps
+
+- See [Type Inference](/core-concepts/type-inference) for practical usage
+- Review [Contracts](/core-concepts/contracts) for defining contracts
+- Explore [HTTP Routes & Schemas](/core-concepts/routes-and-schemas) for HTTP details
+- Read [WebSocket Contracts](/core-concepts/websocket-contracts) for WebSocket details
diff --git a/apps/docs/content/api-reference/meta.json b/apps/docs/content/api-reference/meta.json
new file mode 100644
index 0000000..e4914f7
--- /dev/null
+++ b/apps/docs/content/api-reference/meta.json
@@ -0,0 +1,4 @@
+{
+ "title": "API Reference",
+ "pages": ["core", "plugins"]
+}
diff --git a/apps/docs/content/api-reference/plugins/meta.json b/apps/docs/content/api-reference/plugins/meta.json
new file mode 100644
index 0000000..5a9c203
--- /dev/null
+++ b/apps/docs/content/api-reference/plugins/meta.json
@@ -0,0 +1,4 @@
+{
+ "title": "Plugins API",
+ "pages": ["path-plugin", "validate-plugin"]
+}
diff --git a/apps/docs/content/api-reference/plugins/path-plugin.mdx b/apps/docs/content/api-reference/plugins/path-plugin.mdx
new file mode 100644
index 0000000..672cbf7
--- /dev/null
+++ b/apps/docs/content/api-reference/plugins/path-plugin.mdx
@@ -0,0 +1,354 @@
+---
+title: pathPlugin API
+description: Complete API reference for the pathPlugin.
+---
+
+## Overview
+
+The `pathPlugin` is a built-in plugin that adds the `buildPath()` method to each route for type-safe URL construction.
+
+## Import
+
+```ts
+import { pathPlugin } from '@ts-contract/plugins';
+```
+
+## Plugin Object
+
+```ts
+const pathPlugin: ContractPlugin<'path'>
+```
+
+### Properties
+
+- **name**: `'path'`
+- **route**: Function that adds `buildPath()` method to routes
+
+## Usage
+
+```ts
+import { initContract } from '@ts-contract/core';
+import { pathPlugin } from '@ts-contract/plugins';
+import { contract } from './contract';
+
+const api = initContract(contract)
+ .use(pathPlugin)
+ .build();
+```
+
+## Added Methods
+
+### buildPath()
+
+Builds a URL string with interpolated path parameters and query string.
+
+#### Signature
+
+```ts
+buildPath(...args: BuildPathArgs): string
+```
+
+The exact signature depends on the route definition:
+
+**No parameters:**
+```ts
+buildPath(): string
+```
+
+**Only path parameters:**
+```ts
+buildPath(params: PathParams): string
+```
+
+**Only query parameters:**
+```ts
+buildPath(params?: undefined, query?: Query): string
+```
+
+**Both path and query parameters:**
+```ts
+buildPath(params: PathParams, query?: Query): string
+```
+
+#### Parameters
+
+- **params** - Object with path parameter values (required if route has `pathParams`)
+- **query** - Object with query parameter values (optional if route has `query`)
+
+#### Returns
+
+- Complete URL string with interpolated parameters
+
+#### Examples
+
+**No parameters:**
+
+```ts
+const contract = createContract({
+ listUsers: {
+ method: 'GET',
+ path: '/users',
+ responses: {
+ 200: z.array(z.object({ id: z.string() })),
+ },
+ },
+});
+
+const api = initContract(contract).use(pathPlugin).build();
+
+api.listUsers.buildPath();
+// => "/users"
+```
+
+**Path parameters:**
+
+```ts
+const contract = createContract({
+ getUser: {
+ method: 'GET',
+ path: '/users/:id',
+ pathParams: z.object({ id: z.string() }),
+ responses: {
+ 200: z.object({ id: z.string() }),
+ },
+ },
+});
+
+const api = initContract(contract).use(pathPlugin).build();
+
+api.getUser.buildPath({ id: '123' });
+// => "/users/123"
+```
+
+**Multiple path parameters:**
+
+```ts
+const contract = createContract({
+ getPost: {
+ method: 'GET',
+ path: '/users/:userId/posts/:postId',
+ pathParams: z.object({
+ userId: z.string(),
+ postId: z.string(),
+ }),
+ responses: {
+ 200: z.object({ id: z.string() }),
+ },
+ },
+});
+
+const api = initContract(contract).use(pathPlugin).build();
+
+api.getPost.buildPath({ userId: '123', postId: '456' });
+// => "/users/123/posts/456"
+```
+
+**Query parameters:**
+
+```ts
+const contract = createContract({
+ listUsers: {
+ method: 'GET',
+ path: '/users',
+ query: z.object({
+ page: z.string().optional(),
+ limit: z.string().optional(),
+ }),
+ responses: {
+ 200: z.array(z.object({ id: z.string() })),
+ },
+ },
+});
+
+const api = initContract(contract).use(pathPlugin).build();
+
+api.listUsers.buildPath(undefined, { page: '2', limit: '10' });
+// => "/users?page=2&limit=10"
+```
+
+**Path and query parameters:**
+
+```ts
+const contract = createContract({
+ getUserPosts: {
+ method: 'GET',
+ path: '/users/:userId/posts',
+ pathParams: z.object({ userId: z.string() }),
+ query: z.object({
+ status: z.string().optional(),
+ }),
+ responses: {
+ 200: z.array(z.object({ id: z.string() })),
+ },
+ },
+});
+
+const api = initContract(contract).use(pathPlugin).build();
+
+api.getUserPosts.buildPath({ userId: '123' }, { status: 'published' });
+// => "/users/123/posts?status=published"
+```
+
+## Type Safety
+
+The `buildPath()` method is fully type-safe based on your route definition:
+
+```ts
+const contract = createContract({
+ getUser: {
+ method: 'GET',
+ path: '/users/:id',
+ pathParams: z.object({ id: z.string() }),
+ responses: {
+ 200: z.object({ id: z.string() }),
+ },
+ },
+});
+
+const api = initContract(contract).use(pathPlugin).build();
+
+// ✓ Valid
+api.getUser.buildPath({ id: '123' });
+
+// ✗ Error: Type 'number' is not assignable to type 'string'
+api.getUser.buildPath({ id: 123 });
+
+// ✗ Error: Property 'id' is missing
+api.getUser.buildPath({});
+
+// ✗ Error: Expected 1 arguments, but got 0
+api.getUser.buildPath();
+```
+
+## Behavior
+
+### URL Encoding
+
+Path parameters and query values are automatically URL-encoded:
+
+```ts
+api.getUser.buildPath({ id: 'user@example.com' });
+// => "/users/user%40example.com"
+
+api.searchUsers.buildPath(undefined, { query: 'hello world' });
+// => "/users/search?query=hello+world"
+```
+
+### Optional Query Parameters
+
+Query parameters with `undefined` or `null` values are omitted:
+
+```ts
+const contract = createContract({
+ searchUsers: {
+ method: 'GET',
+ path: '/users/search',
+ query: z.object({
+ name: z.string().optional(),
+ email: z.string().optional(),
+ }),
+ responses: {
+ 200: z.array(z.object({ id: z.string() })),
+ },
+ },
+});
+
+const api = initContract(contract).use(pathPlugin).build();
+
+api.searchUsers.buildPath(undefined, {
+ name: 'Alice',
+ email: undefined, // Omitted
+});
+// => "/users/search?name=Alice"
+```
+
+### Empty Query String
+
+If all query parameters are `undefined` or `null`, no query string is added:
+
+```ts
+api.searchUsers.buildPath(undefined, {
+ name: undefined,
+ email: undefined,
+});
+// => "/users/search"
+```
+
+## Error Handling
+
+### Missing Required Path Parameter
+
+If a required path parameter is missing, a runtime error is thrown:
+
+```ts
+const contract = createContract({
+ getPost: {
+ method: 'GET',
+ path: '/users/:userId/posts/:postId',
+ pathParams: z.object({
+ userId: z.string(),
+ postId: z.string(),
+ }),
+ responses: {
+ 200: z.object({ id: z.string() }),
+ },
+ },
+});
+
+const api = initContract(contract).use(pathPlugin).build();
+
+// Throws: Error: Missing path parameter: postId
+api.getPost.buildPath({ userId: '123' } as any);
+```
+
+TypeScript will catch this at compile time unless you use type assertions.
+
+## Performance
+
+- **Bundle size**: ~500 bytes minified + gzipped
+- **Runtime**: Simple string interpolation and URLSearchParams
+- **Memory**: No state or caching
+
+The plugin has minimal performance impact.
+
+## Type Registry
+
+The plugin registers its types using declaration merging:
+
+```ts
+declare module '@ts-contract/core' {
+ interface PluginTypeRegistry {
+ path: {
+ buildPath: R extends RouteDef
+ ? (...args: BuildPathArgs) => string
+ : never;
+ };
+ }
+}
+```
+
+This enables full type safety for the `buildPath()` method.
+
+## Related Types
+
+### BuildPathArgs
+
+Internal type that determines the `buildPath()` signature based on the route:
+
+```ts
+type BuildPathArgs =
+ InferPathParams extends undefined
+ ? InferQuery extends undefined
+ ? []
+ : [params?: undefined, query?: InferQuery]
+ : InferQuery extends undefined
+ ? [params: InferPathParams]
+ : [params: InferPathParams, query?: InferQuery];
+```
+
+## See Also
+
+- [Path Plugin Guide](/plugins/path-plugin) - Detailed guide with examples
+- [validatePlugin](/api-reference/plugins/validate-plugin) - Runtime validation plugin
+- [initContract](/api-reference/core/init-contract) - Initialize contracts with plugins
+- [Creating Custom Plugins](/plugins/creating-custom-plugins) - Build your own plugins
diff --git a/apps/docs/content/api-reference/plugins/validate-plugin.mdx b/apps/docs/content/api-reference/plugins/validate-plugin.mdx
new file mode 100644
index 0000000..69610f0
--- /dev/null
+++ b/apps/docs/content/api-reference/plugins/validate-plugin.mdx
@@ -0,0 +1,506 @@
+---
+title: validatePlugin API
+description: Complete API reference for the validatePlugin.
+---
+
+## Overview
+
+The `validatePlugin` is a built-in plugin that adds validation methods to each route for runtime schema validation.
+
+## Import
+
+```ts
+import { validatePlugin } from '@ts-contract/plugins';
+```
+
+## Plugin Object
+
+```ts
+const validatePlugin: ContractPlugin<'validate'>
+```
+
+### Properties
+
+- **name**: `'validate'`
+- **route**: Function that adds validation methods to routes
+
+## Usage
+
+```ts
+import { initContract } from '@ts-contract/core';
+import { validatePlugin } from '@ts-contract/plugins';
+import { contract } from './contract';
+
+const api = initContract(contract)
+ .use(validatePlugin)
+ .build();
+```
+
+## Added Methods
+
+### validatePathParams()
+
+Validates path parameters against the route's `pathParams` schema.
+
+#### Signature
+
+```ts
+validatePathParams(params: unknown): InferPathParams
+```
+
+#### Parameters
+
+- **params** - Unknown data to validate
+
+#### Returns
+
+- Validated and typed path parameters
+
+#### Throws
+
+- Error if validation fails or if route has no `pathParams` schema
+
+#### Example
+
+```ts
+const contract = createContract({
+ getUser: {
+ method: 'GET',
+ path: '/users/:id',
+ pathParams: z.object({ id: z.string().uuid() }),
+ responses: {
+ 200: z.object({ id: z.string() }),
+ },
+ },
+});
+
+const api = initContract(contract).use(validatePlugin).build();
+
+// ✓ Valid
+const params = api.getUser.validatePathParams({
+ id: '550e8400-e29b-41d4-a716-446655440000',
+});
+// => { id: '550e8400-e29b-41d4-a716-446655440000' }
+
+// ✗ Throws validation error
+api.getUser.validatePathParams({ id: 'not-a-uuid' });
+```
+
+---
+
+### validateQuery()
+
+Validates query parameters against the route's `query` schema.
+
+#### Signature
+
+```ts
+validateQuery(query: unknown): InferQuery
+```
+
+#### Parameters
+
+- **query** - Unknown data to validate
+
+#### Returns
+
+- Validated and typed query parameters
+
+#### Throws
+
+- Error if validation fails or if route has no `query` schema
+
+#### Example
+
+```ts
+const contract = createContract({
+ listUsers: {
+ method: 'GET',
+ path: '/users',
+ query: z.object({
+ page: z.string().transform(Number),
+ limit: z.string().transform(Number).optional(),
+ }),
+ responses: {
+ 200: z.array(z.object({ id: z.string() })),
+ },
+ },
+});
+
+const api = initContract(contract).use(validatePlugin).build();
+
+// ✓ Valid - transforms strings to numbers
+const query = api.listUsers.validateQuery({ page: '2', limit: '10' });
+// => { page: 2, limit: 10 }
+
+// ✗ Throws validation error
+api.listUsers.validateQuery({ page: 'invalid' });
+```
+
+---
+
+### validateBody()
+
+Validates request body against the route's `body` schema.
+
+#### Signature
+
+```ts
+validateBody(body: unknown): InferBody
+```
+
+#### Parameters
+
+- **body** - Unknown data to validate
+
+#### Returns
+
+- Validated and typed request body
+
+#### Throws
+
+- Error if validation fails or if route has no `body` schema
+
+#### Example
+
+```ts
+const contract = createContract({
+ createUser: {
+ method: 'POST',
+ path: '/users',
+ body: z.object({
+ name: z.string().min(1),
+ email: z.string().email(),
+ }),
+ responses: {
+ 201: z.object({ id: z.string() }),
+ },
+ },
+});
+
+const api = initContract(contract).use(validatePlugin).build();
+
+// ✓ Valid
+const body = api.createUser.validateBody({
+ name: 'Alice',
+ email: 'alice@example.com',
+});
+
+// ✗ Throws validation error
+api.createUser.validateBody({
+ name: '',
+ email: 'not-an-email',
+});
+```
+
+---
+
+### validateResponse()
+
+Validates response data against the route's response schema for a specific status code.
+
+#### Signature
+
+```ts
+validateResponse(
+ status: S,
+ data: unknown
+): InferResponseBody
+```
+
+#### Type Parameters
+
+- **S** - HTTP status code (must be defined in route's `responses`)
+
+#### Parameters
+
+- **status** - HTTP status code
+- **data** - Unknown data to validate
+
+#### Returns
+
+- Validated and typed response body for the specified status code
+
+#### Throws
+
+- Error if validation fails or if route has no response schema for the status code
+
+#### Example
+
+```ts
+const contract = createContract({
+ getUser: {
+ method: 'GET',
+ path: '/users/:id',
+ pathParams: z.object({ id: z.string() }),
+ responses: {
+ 200: z.object({
+ id: z.string(),
+ name: z.string(),
+ email: z.string().email(),
+ }),
+ 404: z.object({
+ message: z.string(),
+ }),
+ },
+ },
+});
+
+const api = initContract(contract).use(validatePlugin).build();
+
+// ✓ Valid - 200 response
+const user = api.getUser.validateResponse(200, {
+ id: '123',
+ name: 'Alice',
+ email: 'alice@example.com',
+});
+
+// ✓ Valid - 404 response
+const error = api.getUser.validateResponse(404, {
+ message: 'User not found',
+});
+
+// ✗ Throws validation error - wrong schema for status
+api.getUser.validateResponse(200, {
+ message: 'User not found',
+});
+```
+
+---
+
+### validateHeaders()
+
+Validates request headers against the route's `headers` schema.
+
+#### Signature
+
+```ts
+validateHeaders(headers: Record): InferHeaders
+```
+
+#### Parameters
+
+- **headers** - Object with header names and values
+
+#### Returns
+
+- Validated and typed headers
+
+#### Throws
+
+- Error if validation fails or if route has no `headers` schema
+
+#### Example
+
+```ts
+const contract = createContract({
+ getProtected: {
+ method: 'GET',
+ path: '/protected',
+ headers: {
+ 'authorization': z.string().startsWith('Bearer '),
+ 'x-api-key': z.string(),
+ },
+ responses: {
+ 200: z.object({ data: z.string() }),
+ },
+ },
+});
+
+const api = initContract(contract).use(validatePlugin).build();
+
+// ✓ Valid
+const headers = api.getProtected.validateHeaders({
+ 'authorization': 'Bearer token123',
+ 'x-api-key': 'key123',
+});
+
+// ✗ Throws validation error
+api.getProtected.validateHeaders({
+ 'authorization': 'token123', // Missing "Bearer " prefix
+ 'x-api-key': 'key123',
+});
+```
+
+## Error Handling
+
+All validation methods throw errors with descriptive messages:
+
+```ts
+try {
+ api.createUser.validateBody({
+ name: '',
+ email: 'invalid-email',
+ });
+} catch (error) {
+ console.error(error.message);
+ // => "Validation failed for body of /users: String must contain at least 1 character(s), Invalid email"
+}
+```
+
+Error messages include:
+- What failed (pathParams, query, body, response, headers)
+- Which route (path)
+- Specific validation issues from the schema library
+
+### Missing Schema Errors
+
+If you try to validate a field that doesn't have a schema:
+
+```ts
+const contract = createContract({
+ getUser: {
+ method: 'GET',
+ path: '/users/:id',
+ // No body schema defined
+ responses: {
+ 200: z.object({ id: z.string() }),
+ },
+ },
+});
+
+const api = initContract(contract).use(validatePlugin).build();
+
+// Throws: Error: Route "/users/:id" has no body schema
+api.getUser.validateBody({ name: 'Alice' });
+```
+
+## Integration with Standard Schema
+
+The validate plugin works with any schema library implementing [@standard-schema/spec](https://github.com/standard-schema/standard-schema):
+
+- **Zod** - `z.object()`, `z.string()`, etc.
+- **Valibot** - `v.object()`, `v.string()`, etc.
+- **Arktype** - `type()` definitions
+- Any custom Standard Schema implementation
+
+## Type Safety
+
+All validation methods return properly typed values:
+
+```ts
+const contract = createContract({
+ createUser: {
+ method: 'POST',
+ path: '/users',
+ body: z.object({
+ name: z.string(),
+ email: z.string().email(),
+ }),
+ responses: {
+ 201: z.object({
+ id: z.string(),
+ name: z.string(),
+ }),
+ },
+ },
+});
+
+const api = initContract(contract).use(validatePlugin).build();
+
+const body = api.createUser.validateBody(unknownData);
+// body is typed as { name: string; email: string }
+
+const response = api.createUser.validateResponse(201, unknownData);
+// response is typed as { id: string; name: string }
+```
+
+## Performance
+
+- **Bundle size**: ~800 bytes minified + gzipped (plus your schema library)
+- **Runtime**: Depends on schema library (Zod, Valibot, Arktype)
+- **Memory**: No state or caching
+
+Validation overhead varies by schema library:
+- **Zod**: ~10-50ms for typical schemas
+- **Valibot**: ~5-20ms (faster)
+- **Arktype**: ~5-15ms (fastest)
+
+## Type Registry
+
+The plugin registers its types using declaration merging:
+
+```ts
+declare module '@ts-contract/core' {
+ interface PluginTypeRegistry {
+ validate: {
+ validatePathParams: R extends RouteDef
+ ? (params: unknown) => InferPathParams
+ : never;
+ validateQuery: R extends RouteDef
+ ? (query: unknown) => InferQuery
+ : never;
+ validateBody: R extends RouteDef
+ ? (body: unknown) => InferBody
+ : never;
+ validateResponse: R extends RouteDef
+ ? (
+ status: S,
+ data: unknown,
+ ) => InferResponseBody
+ : never;
+ validateHeaders: R extends RouteDef
+ ? (headers: Record) => InferHeaders
+ : never;
+ };
+ }
+}
+```
+
+This enables full type safety for all validation methods.
+
+## Common Patterns
+
+### Server-Side Validation
+
+```ts
+import express from 'express';
+
+app.post('/users', (req, res) => {
+ try {
+ const body = api.createUser.validateBody(req.body);
+ const user = database.createUser(body);
+ res.status(201).json(user);
+ } catch (error) {
+ res.status(400).json({ message: error.message });
+ }
+});
+```
+
+### Client-Side Validation
+
+```ts
+async function fetchUser(id: string) {
+ const response = await fetch(`/users/${id}`);
+ const data = await response.json();
+
+ if (response.status === 404) {
+ const error = api.getUser.validateResponse(404, data);
+ throw new Error(error.message);
+ }
+
+ return api.getUser.validateResponse(200, data);
+}
+```
+
+### Conditional Validation
+
+```ts
+const shouldValidate = process.env.NODE_ENV === 'development';
+
+async function fetchUser(id: string) {
+ const response = await fetch(`/users/${id}`);
+ const data = await response.json();
+
+ return shouldValidate
+ ? api.getUser.validateResponse(200, data)
+ : data;
+}
+```
+
+## See Also
+
+- [Validate Plugin Guide](/plugins/validate-plugin) - Detailed guide with examples
+- [pathPlugin](/api-reference/plugins/path-plugin) - URL building plugin
+- [initContract](/api-reference/core/init-contract) - Initialize contracts with plugins
+- [Creating Custom Plugins](/plugins/creating-custom-plugins) - Build your own plugins
diff --git a/apps/docs/content/api-reference/plugins/websocket-plugins.mdx b/apps/docs/content/api-reference/plugins/websocket-plugins.mdx
new file mode 100644
index 0000000..2ac806b
--- /dev/null
+++ b/apps/docs/content/api-reference/plugins/websocket-plugins.mdx
@@ -0,0 +1,299 @@
+---
+title: WebSocket Plugins API
+description: Complete API reference for WebSocket plugins.
+---
+
+## websocketPathPlugin
+
+Adds `buildPath()` method to WebSocket definitions for type-safe URL construction.
+
+### Import
+
+```ts
+import { websocketPathPlugin } from '@ts-contract/plugins';
+```
+
+### Usage
+
+```ts
+const api = initContract(contract)
+ .useWebSocket(websocketPathPlugin)
+ .build();
+```
+
+### Methods
+
+#### buildPath()
+
+Build a WebSocket URL with path parameters and query strings.
+
+**Signature:**
+
+```ts
+buildPath(params?, query?): string
+```
+
+**Parameters:**
+
+- `params` - Path parameters (required if WebSocket has `pathParams`)
+- `query` - Query string parameters (optional if WebSocket has `query`)
+
+**Returns:** Complete URL string with interpolated parameters
+
+**Examples:**
+
+```ts
+// Simple path
+api.chat.buildPath();
+// => "/ws/chat"
+
+// With path parameters
+api.chat.buildPath({ roomId: '123' });
+// => "/ws/chat/123"
+
+// With query parameters
+api.chat.buildPath({ roomId: '123' }, { token: 'abc' });
+// => "/ws/chat/123?token=abc"
+
+// URL encoding
+api.chat.buildPath({ roomId: 'room with spaces' });
+// => "/ws/chat/room%20with%20spaces"
+```
+
+**Type Safety:**
+
+TypeScript enforces correct parameter types based on the WebSocket definition:
+
+```ts
+// ✓ Valid
+api.chat.buildPath({ roomId: '123' });
+
+// ✗ Error: Type 'number' is not assignable to type 'string'
+api.chat.buildPath({ roomId: 123 });
+
+// ✗ Error: Property 'roomId' is missing
+api.chat.buildPath({});
+```
+
+---
+
+## websocketValidatePlugin
+
+Adds validation methods for WebSocket messages and connection parameters.
+
+### Import
+
+```ts
+import { websocketValidatePlugin } from '@ts-contract/plugins';
+```
+
+### Usage
+
+```ts
+const api = initContract(contract)
+ .useWebSocket(websocketValidatePlugin)
+ .build();
+```
+
+### Methods
+
+#### validateClientMessage()
+
+Validates outgoing client messages against the schema.
+
+**Signature:**
+
+```ts
+validateClientMessage(eventName: EventName, data: unknown): InferClientMessage
+```
+
+**Parameters:**
+
+- `eventName` - The event/message name
+- `data` - The message data to validate
+
+**Returns:** Validated and typed message data
+
+**Throws:** Validation error if data doesn't match schema
+
+**Example:**
+
+```ts
+const msg = api.chat.validateClientMessage('new_msg', {
+ type: 'new_msg',
+ body: 'Hello!',
+});
+// msg is typed as: { type: 'new_msg', body: string }
+```
+
+---
+
+#### validateServerMessage()
+
+Validates incoming server messages against the schema.
+
+**Signature:**
+
+```ts
+validateServerMessage(eventName: EventName, data: unknown): InferServerMessage
+```
+
+**Parameters:**
+
+- `eventName` - The event/message name
+- `data` - The message data to validate
+
+**Returns:** Validated and typed message data
+
+**Throws:** Validation error if data doesn't match schema
+
+**Example:**
+
+```ts
+channel.on('new_msg', (data: unknown) => {
+ const msg = api.chat.validateServerMessage('new_msg', data);
+ // msg is typed as: { type: 'new_msg', id: string, body: string, userId: string }
+ console.log(msg.body);
+});
+```
+
+---
+
+#### validatePathParams()
+
+Validates WebSocket connection path parameters.
+
+**Signature:**
+
+```ts
+validatePathParams(params: unknown): InferWebSocketPathParams
+```
+
+**Parameters:**
+
+- `params` - Path parameters to validate
+
+**Returns:** Validated and typed path parameters
+
+**Throws:** Validation error if params don't match schema
+
+**Example:**
+
+```ts
+const params = api.chat.validatePathParams({ roomId: '123' });
+// params is typed as: { roomId: string }
+```
+
+---
+
+#### validateQuery()
+
+Validates WebSocket connection query parameters.
+
+**Signature:**
+
+```ts
+validateQuery(query: unknown): InferWebSocketQuery
+```
+
+**Parameters:**
+
+- `query` - Query parameters to validate
+
+**Returns:** Validated and typed query parameters
+
+**Throws:** Validation error if query doesn't match schema
+
+**Example:**
+
+```ts
+const query = api.chat.validateQuery({ token: 'abc-token' });
+// query is typed as: { token?: string }
+```
+
+---
+
+#### validateHeaders()
+
+Validates WebSocket connection headers.
+
+**Signature:**
+
+```ts
+validateHeaders(headers: Record): InferWebSocketHeaders
+```
+
+**Parameters:**
+
+- `headers` - Headers to validate
+
+**Returns:** Validated and typed headers
+
+**Throws:** Validation error if headers don't match schema
+
+**Example:**
+
+```ts
+const headers = api.chat.validateHeaders({
+ authorization: 'Bearer token123',
+});
+// headers is typed as: { authorization: string }
+```
+
+---
+
+## Error Handling
+
+All validation methods throw descriptive errors when validation fails:
+
+```ts
+try {
+ api.chat.validateClientMessage('new_msg', {
+ type: 'new_msg',
+ body: '', // Empty string fails min length validation
+ });
+} catch (error) {
+ console.error(error.message);
+ // => "Validation failed for client message 'new_msg' of /ws/chat/:roomId: String must contain at least 1 character(s)"
+}
+```
+
+Error messages include:
+- What failed (client message, server message, path params, query, headers)
+- Which WebSocket definition (path)
+- Specific validation issues from your schema library
+
+## Type Inference
+
+All validation methods return properly typed results based on your WebSocket contract:
+
+```ts
+import type {
+ InferClientMessage,
+ InferServerMessage,
+ InferWebSocketPathParams,
+} from '@ts-contract/core';
+
+type ClientMsg = InferClientMessage;
+type ServerMsg = InferServerMessage;
+type Params = InferWebSocketPathParams;
+
+// Validation methods return these exact types
+const msg: ClientMsg = api.chat.validateClientMessage('new_msg', data);
+const serverMsg: ServerMsg = api.chat.validateServerMessage('new_msg', data);
+const params: Params = api.chat.validatePathParams(data);
+```
+
+## Performance
+
+- **websocketPathPlugin**: ~500 bytes minified + gzipped
+- **websocketValidatePlugin**: ~800 bytes minified + gzipped (plus schema library)
+
+Validation overhead depends on your schema library (Zod, Valibot, Arktype).
+
+## See Also
+
+- [WebSocket Plugins Guide](/plugins/websocket-plugins) - Detailed usage guide
+- [WebSocket Contracts](/core-concepts/websocket-contracts) - Contract definition
+- [Type Helpers](/api-reference/core/type-helpers) - Type inference utilities
+- [Phoenix.js Chat Recipe](/recipes/websocket/phoenix-chat) - Complete example
diff --git a/apps/docs/content/contract/helpers.mdx b/apps/docs/content/contract/helpers.mdx
deleted file mode 100644
index 43f3733..0000000
--- a/apps/docs/content/contract/helpers.mdx
+++ /dev/null
@@ -1,10 +0,0 @@
----
-title: Helpers
-description: Utility helpers available for contracts.
----
-
-This page is a starter for helper utilities.
-
-- Builder helpers
-- Reusable schema helpers
-- Validation helpers
diff --git a/apps/docs/content/contract/meta.json b/apps/docs/content/contract/meta.json
deleted file mode 100644
index 460ba51..0000000
--- a/apps/docs/content/contract/meta.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "title": "Contract",
- "pages": ["overview", "helpers", "plugin-interface"]
-}
diff --git a/apps/docs/content/contract/overview.mdx b/apps/docs/content/contract/overview.mdx
deleted file mode 100644
index ff3871e..0000000
--- a/apps/docs/content/contract/overview.mdx
+++ /dev/null
@@ -1,10 +0,0 @@
----
-title: Overview
-description: Core concepts of ts-contract contracts.
----
-
-This page is a starter for the contract overview.
-
-- What a contract is
-- How schemas are attached
-- How routes are composed
diff --git a/apps/docs/content/contract/plugin-interface.mdx b/apps/docs/content/contract/plugin-interface.mdx
deleted file mode 100644
index 61bf5f3..0000000
--- a/apps/docs/content/contract/plugin-interface.mdx
+++ /dev/null
@@ -1,10 +0,0 @@
----
-title: Plugin Interface
-description: Extend ts-contract using plugins.
----
-
-This page is a starter for plugin interface docs.
-
-- Plugin shape
-- Lifecycle hooks
-- Extension points
diff --git a/apps/docs/content/core-concepts/contracts.mdx b/apps/docs/content/core-concepts/contracts.mdx
new file mode 100644
index 0000000..d03eb87
--- /dev/null
+++ b/apps/docs/content/core-concepts/contracts.mdx
@@ -0,0 +1,212 @@
+---
+title: Contracts
+description: Understanding contracts in ts-contract and how to define type-safe API specifications.
+---
+
+## What is a Contract?
+
+A **contract** in ts-contract is a type-safe specification of your HTTP API. It defines the shape of your routes, including paths, methods, parameters, request bodies, and responses. Contracts serve as the single source of truth shared between your server and client code.
+
+Unlike traditional API specifications (like OpenAPI), ts-contract contracts are written in TypeScript and provide first-class type inference throughout your application.
+
+## Creating a Basic Contract
+
+Use `createContract()` to define your API contract:
+
+```ts
+import { createContract } from '@ts-contract/core';
+import { z } from 'zod';
+
+const contract = createContract({
+ getUser: {
+ method: 'GET',
+ path: '/users/:id',
+ pathParams: z.object({ id: z.string() }),
+ responses: {
+ 200: z.object({ id: z.string(), name: z.string() }),
+ 404: z.object({ message: z.string() }),
+ },
+ },
+});
+```
+
+## Understanding ContractDef
+
+The `createContract()` function accepts a `ContractDef` - an object where each key is either:
+
+1. **A route definition** (`RouteDef`) - defines a single HTTP endpoint
+2. **A WebSocket definition** (`WebSocketDef`) - defines a WebSocket connection
+3. **A nested contract** (`ContractDef`) - groups related definitions together
+
+This flexibility allows you to organize your API in a way that makes sense for your application.
+
+## Definition Types
+
+### HTTP Route Definition
+
+A route definition describes a single HTTP endpoint:
+
+```ts
+const contract = createContract({
+ getUser: {
+ method: 'GET',
+ path: '/users/:id',
+ pathParams: z.object({ id: z.string() }),
+ responses: {
+ 200: z.object({ id: z.string(), name: z.string() }),
+ },
+ },
+});
+```
+
+### WebSocket Definition
+
+A WebSocket definition describes a WebSocket connection with bidirectional messages:
+
+```ts
+const contract = createContract({
+ chat: {
+ type: 'websocket',
+ path: '/ws/chat/:roomId',
+ pathParams: z.object({ roomId: z.string() }),
+ clientMessages: {
+ new_msg: z.object({
+ type: z.literal('new_msg'),
+ body: z.string(),
+ }),
+ },
+ serverMessages: {
+ new_msg: z.object({
+ type: z.literal('new_msg'),
+ id: z.string(),
+ body: z.string(),
+ }),
+ },
+ },
+});
+```
+
+[Learn more about WebSocket contracts →](/core-concepts/websocket-contracts)
+
+### Nested Contracts
+
+Nest contracts to organize related routes:
+
+```ts
+const contract = createContract({
+ users: {
+ getUser: {
+ method: 'GET',
+ path: '/users/:id',
+ pathParams: z.object({ id: z.string() }),
+ responses: {
+ 200: z.object({ id: z.string(), name: z.string() }),
+ },
+ },
+ listUsers: {
+ method: 'GET',
+ path: '/users',
+ query: z.object({ page: z.string().optional() }),
+ responses: {
+ 200: z.array(z.object({ id: z.string(), name: z.string() })),
+ },
+ },
+ },
+ posts: {
+ getPost: {
+ method: 'GET',
+ path: '/posts/:id',
+ pathParams: z.object({ id: z.string() }),
+ responses: {
+ 200: z.object({ id: z.string(), title: z.string() }),
+ },
+ },
+ },
+});
+```
+
+Access nested routes using dot notation:
+
+```ts
+// After building with plugins
+api.users.getUser.buildPath({ id: '123' });
+api.posts.getPost.buildPath({ id: '456' });
+```
+
+## Composing Contracts
+
+Compose multiple contracts for better organization:
+
+```ts
+const userContract = createContract({
+ getUser: { method: 'GET', path: '/users/:id', /* ... */ },
+ createUser: { method: 'POST', path: '/users', /* ... */ },
+});
+
+const postContract = createContract({
+ getPost: { method: 'GET', path: '/posts/:id', /* ... */ },
+ createPost: { method: 'POST', path: '/posts', /* ... */ },
+});
+
+const apiContract = createContract({
+ users: userContract,
+ posts: postContract,
+});
+```
+
+This allows you to keep related routes together in separate files and maintain clean separation of concerns.
+
+## Best Practices for Contract Organization
+
+### Share Common Schemas
+
+Extract reusable schemas to avoid duplication:
+
+```ts
+const UserSchema = z.object({ id: z.string(), name: z.string() });
+const ErrorSchema = z.object({ message: z.string() });
+
+const contract = createContract({
+ getUser: {
+ method: 'GET',
+ path: '/users/:id',
+ responses: { 200: UserSchema, 404: ErrorSchema },
+ },
+});
+```
+
+### Split Large APIs
+
+Organize contracts by resource and compose them:
+
+```ts
+const userContract = createContract({ /* user routes */ });
+const postContract = createContract({ /* post routes */ });
+
+const apiContract = createContract({
+ users: userContract,
+ posts: postContract,
+});
+```
+
+## Mixed HTTP and WebSocket Contracts
+
+Combine HTTP routes and WebSocket definitions in the same contract:
+
+```ts
+const contract = createContract({
+ http: {
+ getUser: { method: 'GET', path: '/users/:id', /* ... */ },
+ },
+ ws: {
+ chat: { type: 'websocket', path: '/ws/chat', /* ... */ },
+ },
+});
+```
+
+## Next Steps
+
+- Learn about [HTTP Routes & Schemas](/core-concepts/routes-and-schemas) for HTTP endpoint details
+- Explore [WebSocket Contracts](/core-concepts/websocket-contracts) for real-time communication
+- See [Type Inference](/core-concepts/type-inference) to extract types from your contracts
+- Understand the [Plugin System](/core-concepts/plugin-system) to add functionality
diff --git a/apps/docs/content/core-concepts/meta.json b/apps/docs/content/core-concepts/meta.json
new file mode 100644
index 0000000..26f5988
--- /dev/null
+++ b/apps/docs/content/core-concepts/meta.json
@@ -0,0 +1,4 @@
+{
+ "title": "Core Concepts",
+ "pages": ["contracts", "routes-and-schemas", "type-inference", "plugin-system"]
+}
diff --git a/apps/docs/content/core-concepts/plugin-system.mdx b/apps/docs/content/core-concepts/plugin-system.mdx
new file mode 100644
index 0000000..5234853
--- /dev/null
+++ b/apps/docs/content/core-concepts/plugin-system.mdx
@@ -0,0 +1,266 @@
+---
+title: Plugin System
+description: Extend your contracts with composable plugins for runtime utilities and enhanced functionality.
+---
+
+## Plugin System Overview
+
+The plugin system in ts-contract allows you to extend your contracts with runtime utilities while maintaining full type safety. Plugins add methods to each route in your contract, enabling features like path building, validation, and custom functionality.
+
+**Philosophy:** Plugins are opt-in and composable. Start with a minimal contract and add only the capabilities you need.
+
+## Basic Plugin Usage
+
+Use `initContract()` to create a builder, compose plugins with `.use()`, and call `.build()` to produce an enhanced contract:
+
+```ts
+import { createContract, initContract } from '@ts-contract/core';
+import { pathPlugin, validatePlugin } from '@ts-contract/plugins';
+import { z } from 'zod';
+
+const contract = createContract({
+ getUser: {
+ method: 'GET',
+ path: '/users/:id',
+ pathParams: z.object({ id: z.string() }),
+ responses: {
+ 200: z.object({
+ id: z.string(),
+ name: z.string(),
+ email: z.string().email(),
+ }),
+ },
+ },
+});
+
+// Initialize and add plugins
+const api = initContract(contract)
+ .use(pathPlugin)
+ .use(validatePlugin)
+ .build();
+
+// Now routes have plugin methods
+const url = api.getUser.buildPath({ id: '123' });
+// => "/users/123"
+
+const user = api.getUser.validateResponse(200, {
+ id: '123',
+ name: 'Alice',
+ email: 'alice@example.com',
+});
+// => { id: '123', name: 'Alice', email: 'alice@example.com' }
+```
+
+
+## How Plugins Extend Routes
+
+Plugins add methods to each route in your contract. The methods are fully type-safe based on the route definition:
+
+```ts
+import { createContract, initContract } from '@ts-contract/core';
+import { pathPlugin, validatePlugin } from '@ts-contract/plugins';
+import { z } from 'zod';
+
+const contract = createContract({
+ getUser: {
+ method: 'GET',
+ path: '/users/:id',
+ pathParams: z.object({ id: z.string() }),
+ query: z.object({ include: z.string().optional() }),
+ responses: {
+ 200: z.object({ id: z.string(), name: z.string() }),
+ },
+ },
+});
+
+const api = initContract(contract)
+ .use(pathPlugin)
+ .use(validatePlugin)
+ .build();
+
+// pathPlugin adds buildPath()
+api.getUser.buildPath({ id: '123' });
+api.getUser.buildPath({ id: '123' }, { include: 'profile' });
+
+// validatePlugin adds validation methods
+api.getUser.validatePathParams({ id: '123' });
+api.getUser.validateQuery({ include: 'profile' });
+api.getUser.validateResponse(200, { id: '123', name: 'Alice' });
+```
+
+## Plugin Execution Order
+
+Plugins are applied in the order you call `.use()`. If plugins add methods with the same name, later plugins override earlier ones.
+
+## Type Safety with Plugins
+
+Plugins use TypeScript's declaration merging to provide full type safety. The types are automatically inferred based on your route definitions:
+
+```ts
+const contract = createContract({
+ getUser: {
+ method: 'GET',
+ path: '/users/:id',
+ pathParams: z.object({ id: z.string() }),
+ responses: {
+ 200: z.object({ id: z.string(), name: z.string() }),
+ 404: z.object({ message: z.string() }),
+ },
+ },
+});
+
+const api = initContract(contract)
+ .use(pathPlugin)
+ .use(validatePlugin)
+ .build();
+
+// TypeScript knows the exact parameter types
+api.getUser.buildPath({ id: '123' }); // ✓ Valid
+api.getUser.buildPath({ id: 123 }); // ✗ Error: id must be string
+
+// TypeScript knows the response types by status code
+api.getUser.validateResponse(200, { id: '123', name: 'Alice' }); // ✓ Valid
+api.getUser.validateResponse(200, { id: '123' }); // ✗ Error: missing name
+api.getUser.validateResponse(404, { message: 'Not found' }); // ✓ Valid
+```
+
+## Built-in Plugins
+
+ts-contract provides two built-in plugins:
+
+### pathPlugin
+
+Adds `buildPath()` method for type-safe URL construction:
+
+```ts
+import { pathPlugin } from '@ts-contract/plugins';
+
+const api = initContract(contract)
+ .use(pathPlugin)
+ .build();
+
+// Build paths with parameters
+api.getUser.buildPath({ id: '123' });
+// => "/users/123"
+
+// Build paths with query strings
+api.listUsers.buildPath(undefined, { page: '2', limit: '10' });
+// => "/users?page=2&limit=10"
+```
+
+Learn more: [Path Plugin](/plugins/path-plugin)
+
+### validatePlugin
+
+Adds validation methods for runtime schema validation:
+
+```ts
+import { validatePlugin } from '@ts-contract/plugins';
+
+const api = initContract(contract)
+ .use(validatePlugin)
+ .build();
+
+// Validate request data
+api.createUser.validateBody({ name: 'Alice', email: 'alice@example.com' });
+
+// Validate response data
+api.getUser.validateResponse(200, responseData);
+```
+
+Learn more: [Validate Plugin](/plugins/validate-plugin)
+
+## WebSocket Plugins
+
+WebSocket definitions use separate plugins via the `.useWebSocket()` method:
+
+```ts
+import { websocketPathPlugin, websocketValidatePlugin } from '@ts-contract/plugins';
+
+const contract = createContract({
+ chat: {
+ type: 'websocket',
+ path: '/ws/chat/:roomId',
+ pathParams: z.object({ roomId: z.string() }),
+ clientMessages: {
+ new_msg: z.object({
+ type: z.literal('new_msg'),
+ body: z.string(),
+ }),
+ },
+ serverMessages: {
+ new_msg: z.object({
+ type: z.literal('new_msg'),
+ id: z.string(),
+ body: z.string(),
+ }),
+ },
+ },
+});
+
+const api = initContract(contract)
+ .useWebSocket(websocketPathPlugin)
+ .useWebSocket(websocketValidatePlugin)
+ .build();
+
+// Build WebSocket URL
+api.chat.buildPath({ roomId: '123' });
+// => "/ws/chat/123"
+
+// Validate messages
+api.chat.validateClientMessage('new_msg', { type: 'new_msg', body: 'Hello!' });
+api.chat.validateServerMessage('new_msg', data);
+```
+
+### WebSocket Plugin Methods
+
+**websocketPathPlugin** adds:
+- `buildPath()` - Build WebSocket URLs with path parameters and query strings
+
+**websocketValidatePlugin** adds:
+- `validateClientMessage()` - Validate outgoing messages
+- `validateServerMessage()` - Validate incoming messages
+- `validatePathParams()` - Validate connection path parameters
+- `validateQuery()` - Validate connection query parameters
+- `validateHeaders()` - Validate connection headers
+
+Learn more: [WebSocket Plugins](/plugins/websocket-plugins)
+
+## Using Both Plugins Together
+
+Most applications use both plugins:
+
+```ts
+export const api = initContract(contract)
+ .use(pathPlugin)
+ .use(validatePlugin)
+ .build();
+
+// Client usage
+const url = api.getUser.buildPath({ id });
+const data = await fetch(url).then(r => r.json());
+return api.getUser.validateResponse(200, data);
+```
+
+
+## When to Use Plugins
+
+**Use plugins when:**
+- You want runtime utilities (path building, validation)
+- You need consistent behavior across all routes
+- You're building a client library or SDK
+
+**Skip plugins when:**
+- You only need type inference (no runtime utilities)
+- You want absolute minimal bundle size
+
+## Creating Custom Plugins
+
+You can create your own plugins to add custom functionality. See [Creating Custom Plugins](/plugins/creating-custom-plugins) for a detailed guide.
+
+## Next Steps
+
+- Explore [Path Plugin](/plugins/path-plugin) and [WebSocket Plugins](/plugins/websocket-plugins)
+- Learn about [Validate Plugin](/plugins/validate-plugin) for runtime validation
+- See [Creating Custom Plugins](/plugins/creating-custom-plugins) to build your own
+- Review [Best Practices](/guides/best-practices) for plugin usage patterns
diff --git a/apps/docs/content/core-concepts/routes-and-schemas.mdx b/apps/docs/content/core-concepts/routes-and-schemas.mdx
new file mode 100644
index 0000000..5eccdfa
--- /dev/null
+++ b/apps/docs/content/core-concepts/routes-and-schemas.mdx
@@ -0,0 +1,340 @@
+---
+title: HTTP Routes & Schemas
+description: Deep dive into HTTP route definitions and schema integration in ts-contract.
+---
+
+## HTTP Route Definition Anatomy
+
+A route definition (`RouteDef`) describes a single HTTP endpoint with all its inputs and outputs.
+
+> **WebSocket Contracts**: For WebSocket connections, see [WebSocket Contracts](/core-concepts/websocket-contracts) which use a different structure with bidirectional message schemas.
+
+Here's a fully annotated example:
+
+```ts
+import { createContract } from '@ts-contract/core';
+import { z } from 'zod';
+
+const contract = createContract({
+ updateUser: {
+ // HTTP method
+ method: 'PUT',
+
+ // URL path with parameter placeholders
+ path: '/users/:id',
+
+ // Schema for path parameters
+ pathParams: z.object({
+ id: z.string()
+ }),
+
+ // Schema for query string parameters
+ query: z.object({
+ notify: z.boolean().optional()
+ }),
+
+ // Schema for request headers
+ headers: {
+ 'x-api-key': z.string(),
+ },
+
+ // Schema for request body
+ body: z.object({
+ name: z.string(),
+ email: z.string().email(),
+ }),
+
+ // Schemas for different response status codes
+ responses: {
+ 200: z.object({
+ id: z.string(),
+ name: z.string(),
+ email: z.string().email(),
+ updatedAt: z.string(),
+ }),
+ 400: z.object({
+ message: z.string(),
+ errors: z.array(z.string()).optional(),
+ }),
+ 401: z.object({
+ message: z.string()
+ }),
+ 404: z.object({
+ message: z.string()
+ }),
+ },
+
+ // Optional: Human-readable summary
+ summary: 'Update a user profile',
+
+ // Optional: Custom metadata
+ metadata: {
+ requiresAuth: true,
+ rateLimit: 100,
+ },
+ },
+});
+```
+
+## Route Definition Fields
+
+### method (required)
+
+The HTTP method for this endpoint:
+
+```ts
+type Method = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS' | 'HEAD';
+```
+
+### path (required)
+
+The URL path template with optional parameter placeholders using `:paramName` syntax:
+
+```ts
+path: '/users' // Static path
+path: '/users/:id' // Single parameter
+path: '/users/:userId/posts/:postId' // Multiple parameters
+```
+
+### pathParams (optional)
+
+Schema for validating path parameters. Parameter names must match the placeholders in `path`:
+
+```ts
+{
+ path: '/users/:userId/posts/:postId',
+ pathParams: z.object({
+ userId: z.string(),
+ postId: z.string(),
+ }),
+}
+```
+
+### query (optional)
+
+Schema for query string parameters:
+
+```ts
+{
+ query: z.object({
+ page: z.string().optional(),
+ limit: z.string().optional(),
+ sort: z.enum(['asc', 'desc']).optional(),
+ }),
+}
+```
+
+### headers (optional)
+
+Schema for request headers as a record of header names to schemas:
+
+```ts
+{
+ headers: {
+ 'authorization': z.string(),
+ 'x-api-key': z.string(),
+ 'content-type': z.literal('application/json'),
+ },
+}
+```
+
+Header names are case-insensitive in HTTP but should be lowercase in your contract.
+
+### body (optional)
+
+Schema for the request body:
+
+```ts
+{
+ method: 'POST',
+ body: z.object({
+ name: z.string(),
+ email: z.string().email(),
+ age: z.number().int().min(0),
+ }),
+}
+```
+
+Typically used with `POST`, `PUT`, and `PATCH` methods.
+
+### responses (required)
+
+Schemas for different HTTP status codes. At least one response must be defined:
+
+```ts
+{
+ responses: {
+ 200: z.object({ success: z.boolean() }),
+ 400: z.object({ message: z.string() }),
+ 500: z.object({ message: z.string() }),
+ },
+}
+```
+
+### summary (optional)
+
+Human-readable description of the endpoint:
+
+```ts
+{
+ summary: 'Retrieve a user by ID',
+}
+```
+
+Useful for documentation generation and developer reference.
+
+### metadata (optional)
+
+Custom metadata as key-value pairs:
+
+```ts
+{
+ metadata: {
+ requiresAuth: true,
+ rateLimit: 100,
+ version: 'v1',
+ deprecated: false,
+ },
+}
+```
+
+Use metadata for custom tooling, documentation, or runtime behavior.
+
+## Standard Schema Protocol
+
+ts-contract uses the [@standard-schema/spec](https://github.com/standard-schema/standard-schema) protocol, which means it works with any compliant validation library.
+
+### Supported Libraries
+
+ts-contract works with Zod, Valibot, Arktype, and any Standard Schema compliant library:
+
+```ts
+import { z } from 'zod';
+
+const contract = createContract({
+ getUser: {
+ method: 'GET',
+ path: '/users/:id',
+ pathParams: z.object({ id: z.string().uuid() }),
+ responses: {
+ 200: z.object({
+ id: z.string().uuid(),
+ name: z.string().min(1),
+ email: z.string().email(),
+ }),
+ },
+ },
+});
+```
+
+## Response Status Codes
+
+Define multiple response schemas for different status codes:
+
+```ts
+{
+ responses: {
+ 200: z.object({ data: z.any() }),
+ 201: z.object({ id: z.string(), createdAt: z.string() }),
+ 204: z.null(),
+ 400: z.object({ message: z.string() }),
+ 404: z.object({ message: z.string() }),
+ 500: z.object({ message: z.string() }),
+ },
+}
+```
+
+## Common CRUD Patterns
+
+### List Resources
+
+```ts
+{
+ listUsers: {
+ method: 'GET',
+ path: '/users',
+ query: z.object({
+ page: z.string().optional(),
+ limit: z.string().optional(),
+ }),
+ responses: {
+ 200: z.object({
+ data: z.array(z.object({ id: z.string(), name: z.string() })),
+ total: z.number(),
+ }),
+ },
+ },
+}
+```
+
+### Get Single Resource
+
+```ts
+{
+ getUser: {
+ method: 'GET',
+ path: '/users/:id',
+ pathParams: z.object({ id: z.string() }),
+ responses: {
+ 200: z.object({ id: z.string(), name: z.string(), email: z.string() }),
+ 404: z.object({ message: z.string() }),
+ },
+ },
+}
+```
+
+### Create Resource
+
+```ts
+{
+ createUser: {
+ method: 'POST',
+ path: '/users',
+ body: z.object({ name: z.string(), email: z.string().email() }),
+ responses: {
+ 201: z.object({ id: z.string(), name: z.string(), email: z.string() }),
+ 400: z.object({ message: z.string() }),
+ },
+ },
+}
+```
+
+### Update Resource
+
+```ts
+{
+ updateUser: {
+ method: 'PUT',
+ path: '/users/:id',
+ pathParams: z.object({ id: z.string() }),
+ body: z.object({ name: z.string(), email: z.string().email() }),
+ responses: {
+ 200: z.object({ id: z.string(), name: z.string(), email: z.string() }),
+ 404: z.object({ message: z.string() }),
+ },
+ },
+}
+```
+
+### Delete Resource
+
+```ts
+{
+ deleteUser: {
+ method: 'DELETE',
+ path: '/users/:id',
+ pathParams: z.object({ id: z.string() }),
+ responses: {
+ 204: z.null(),
+ 404: z.object({ message: z.string() }),
+ },
+ },
+}
+```
+
+## Next Steps
+
+- Explore [WebSocket Contracts](/core-concepts/websocket-contracts) for real-time communication
+- Learn about [Type Inference](/core-concepts/type-inference) to extract types from your routes
+- Understand the [Plugin System](/core-concepts/plugin-system) to add functionality
+- See [Contracts](/core-concepts/contracts) for organizing multiple routes
diff --git a/apps/docs/content/core-concepts/type-inference.mdx b/apps/docs/content/core-concepts/type-inference.mdx
new file mode 100644
index 0000000..745e5e5
--- /dev/null
+++ b/apps/docs/content/core-concepts/type-inference.mdx
@@ -0,0 +1,258 @@
+---
+title: Type Inference
+description: Extract and use TypeScript types from your ts-contract routes for end-to-end type safety.
+---
+
+## How Type Inference Works
+
+ts-contract provides powerful TypeScript type helpers that extract types from your route definitions. This enables end-to-end type safety from your contract to both server and client code without any code generation or build steps.
+
+All type inference happens at compile time using TypeScript's type system - there's no runtime overhead.
+
+## Example Contract
+
+We'll use this contract throughout the examples:
+
+```ts
+import { createContract } from '@ts-contract/core';
+import { z } from 'zod';
+
+const contract = createContract({
+ getUser: {
+ method: 'GET',
+ path: '/users/:id',
+ pathParams: z.object({ id: z.string() }),
+ responses: {
+ 200: z.object({ id: z.string(), name: z.string(), email: z.string() }),
+ 404: z.object({ message: z.string() }),
+ },
+ },
+ listUsers: {
+ method: 'GET',
+ path: '/users',
+ query: z.object({
+ page: z.string().optional(),
+ limit: z.string().optional(),
+ }),
+ responses: {
+ 200: z.object({ users: z.array(z.object({ id: z.string(), name: z.string() })), total: z.number() }),
+ },
+ },
+ createUser: {
+ method: 'POST',
+ path: '/users',
+ body: z.object({ name: z.string(), email: z.string().email() }),
+ headers: { 'authorization': z.string() },
+ responses: {
+ 201: z.object({ id: z.string(), name: z.string(), email: z.string() }),
+ 400: z.object({ message: z.string() }),
+ },
+ },
+ updateUser: {
+ method: 'PUT',
+ path: '/users/:id',
+ pathParams: z.object({ id: z.string() }),
+ query: z.object({ notify: z.boolean().optional() }),
+ body: z.object({ name: z.string(), email: z.string() }),
+ responses: {
+ 200: z.object({ id: z.string(), name: z.string(), email: z.string() }),
+ },
+ },
+});
+```
+
+## Available Type Helpers
+
+ts-contract exports type helpers for both HTTP and WebSocket contracts from `@ts-contract/core`:
+
+**HTTP Type Helpers:**
+```ts
+import type {
+ InferPathParams,
+ InferQuery,
+ InferBody,
+ InferHeaders,
+ InferResponseBody,
+ InferResponses,
+ InferArgs,
+} from '@ts-contract/core';
+```
+
+**WebSocket Type Helpers:**
+```ts
+import type {
+ InferWebSocketPathParams,
+ InferWebSocketQuery,
+ InferWebSocketHeaders,
+ InferClientMessages,
+ InferServerMessages,
+ InferClientMessage,
+ InferServerMessage,
+} from '@ts-contract/core';
+```
+
+## InferPathParams
+
+Extract path parameter types from a route:
+
+```ts
+type Params = InferPathParams;
+// => { id: string }
+```
+
+## InferQuery
+
+Extract query parameter types from a route:
+
+```ts
+type Query = InferQuery;
+// => { page?: string; limit?: string }
+```
+
+## InferBody
+
+Extract request body type from a route:
+
+```ts
+type Body = InferBody;
+// => { name: string; email: string }
+```
+
+## InferHeaders
+
+Extract request header types from a route:
+
+```ts
+type Headers = InferHeaders;
+// => { authorization: string }
+```
+
+## InferResponseBody
+
+Extract a specific response type by status code:
+
+```ts
+type User = InferResponseBody;
+// => { id: string; name: string; email: string }
+
+type NotFound = InferResponseBody;
+// => { message: string }
+```
+
+## InferResponses
+
+Extract all responses as a discriminated union:
+
+```ts
+type Response = InferResponses;
+// =>
+// | { status: 200; body: { id: string; name: string; email: string } }
+// | { status: 404; body: { message: string } }
+```
+
+## InferArgs
+
+Merge all input types (params, query, body, headers) into a single object:
+
+```ts
+type Args = InferArgs;
+// => {
+// params: { id: string };
+// query: { notify?: boolean };
+// body: { name: string; email: string };
+// }
+```
+
+## WebSocket Type Inference
+
+Extract types from WebSocket definitions:
+
+```ts
+import { createContract } from '@ts-contract/core';
+import type {
+ InferWebSocketPathParams,
+ InferClientMessage,
+ InferServerMessage,
+} from '@ts-contract/core';
+import { z } from 'zod';
+
+const contract = createContract({
+ chat: {
+ type: 'websocket',
+ path: '/ws/chat/:roomId',
+ pathParams: z.object({ roomId: z.string() }),
+ clientMessages: {
+ new_msg: z.object({
+ type: z.literal('new_msg'),
+ body: z.string(),
+ }),
+ },
+ serverMessages: {
+ new_msg: z.object({
+ type: z.literal('new_msg'),
+ id: z.string(),
+ body: z.string(),
+ }),
+ },
+ },
+});
+
+// Infer path parameters
+type Params = InferWebSocketPathParams;
+// => { roomId: string }
+
+// Infer specific message types
+type ClientMsg = InferClientMessage;
+// => { type: 'new_msg', body: string }
+
+type ServerMsg = InferServerMessage;
+// => { type: 'new_msg', id: string, body: string }
+```
+
+Learn more: [WebSocket Contracts](/core-concepts/websocket-contracts)
+
+## Usage Examples
+
+For complete usage examples, see:
+
+- **Server-side**: [Express Integration](/recipes/server/express), [Hono Integration](/recipes/server/hono)
+- **Client-side**: [React Query Integration](/recipes/client/react-query)
+- **WebSocket**: [Phoenix.js Chat](/recipes/websocket/phoenix-chat)
+
+## Tips for Type Inference
+
+### Use `typeof` to Reference Definitions
+
+Always use `typeof` when extracting types:
+
+```ts
+type Params = InferPathParams;
+type WsParams = InferWebSocketPathParams;
+```
+
+### Extract Types at Module Level
+
+Define types at the module level for reuse:
+
+```ts
+export type User = InferResponseBody;
+export type ChatMessage = InferClientMessage;
+```
+
+### Combine with Utility Types
+
+TypeScript utility types work with inferred types:
+
+```ts
+type User = InferResponseBody;
+type PartialUser = Partial;
+type NewUser = Omit;
+```
+
+
+## Next Steps
+
+- Learn about the [Plugin System](/core-concepts/plugin-system) to add runtime utilities
+- See [Contracts](/core-concepts/contracts) for organizing your API
+- Explore [HTTP Routes & Schemas](/core-concepts/routes-and-schemas) for HTTP details
+- Read [WebSocket Contracts](/core-concepts/websocket-contracts) for WebSocket details
diff --git a/apps/docs/content/core-concepts/websocket-contracts.mdx b/apps/docs/content/core-concepts/websocket-contracts.mdx
new file mode 100644
index 0000000..1ca3756
--- /dev/null
+++ b/apps/docs/content/core-concepts/websocket-contracts.mdx
@@ -0,0 +1,206 @@
+---
+title: WebSocket Contracts
+description: Define type-safe WebSocket APIs with bidirectional message schemas
+---
+
+# WebSocket Contracts
+
+WebSocket contracts allow you to define type-safe WebSocket APIs with full TypeScript inference for bidirectional messages, connection parameters, and event types.
+
+## Overview
+
+Unlike HTTP routes which follow a request-response pattern, WebSocket connections are:
+
+- **Bidirectional**: Both client→server and server→client messages
+- **Event-based**: Multiple message types per connection
+- **Stateful**: Long-lived connections with lifecycle events
+
+ts-contract models WebSocket connections with separate schemas for client and server messages, while letting external frameworks (Phoenix.js, Socket.io, etc.) handle connection lifecycle.
+
+## Basic WebSocket Definition
+
+A WebSocket definition includes:
+
+- **type**: Must be `'websocket'` (discriminator)
+- **path**: Connection endpoint (supports path parameters)
+- **pathParams**: Optional path parameter schema
+- **query**: Optional query parameter schema
+- **headers**: Optional header schemas
+- **clientMessages**: Client→Server message schemas (keyed by event name)
+- **serverMessages**: Server→Client message schemas (keyed by event name)
+
+```typescript
+import { createContract } from '@ts-contract/core';
+import { z } from 'zod';
+
+const contract = createContract({
+ chat: {
+ type: 'websocket',
+ path: '/ws/chat/:roomId',
+ pathParams: z.object({ roomId: z.string() }),
+ query: z.object({ token: z.string() }),
+ clientMessages: {
+ new_msg: z.object({
+ type: z.literal('new_msg'),
+ body: z.string(),
+ }),
+ typing: z.object({
+ type: z.literal('typing'),
+ isTyping: z.boolean(),
+ }),
+ },
+ serverMessages: {
+ new_msg: z.object({
+ type: z.literal('new_msg'),
+ id: z.string(),
+ body: z.string(),
+ userId: z.string(),
+ }),
+ user_typing: z.object({
+ type: z.literal('user_typing'),
+ userId: z.string(),
+ isTyping: z.boolean(),
+ }),
+ },
+ },
+});
+```
+
+## Message Type Discriminators
+
+**Important**: Message schemas must include a type discriminator field that matches the event name. This ensures type safety and runtime validation.
+
+```typescript
+clientMessages: {
+ // Event name: 'new_msg'
+ new_msg: z.object({
+ type: z.literal('new_msg'), // Must match event name
+ body: z.string(),
+ }),
+}
+```
+
+This pattern works seamlessly with frameworks like Phoenix.js that use event-based messaging.
+
+## Mixed HTTP and WebSocket Contracts
+
+You can combine HTTP routes and WebSocket definitions in the same contract:
+
+```typescript
+const contract = createContract({
+ http: {
+ getUser: {
+ method: 'GET',
+ path: '/users/:id',
+ pathParams: z.object({ id: z.string() }),
+ responses: {
+ 200: z.object({ name: z.string() }),
+ },
+ },
+ },
+ ws: {
+ chat: {
+ type: 'websocket',
+ path: '/ws/chat/:roomId',
+ pathParams: z.object({ roomId: z.string() }),
+ clientMessages: {
+ new_msg: z.object({
+ type: z.literal('new_msg'),
+ body: z.string(),
+ }),
+ },
+ serverMessages: {
+ new_msg: z.object({
+ type: z.literal('new_msg'),
+ id: z.string(),
+ body: z.string(),
+ }),
+ },
+ },
+ },
+});
+```
+
+## Using WebSocket Plugins
+
+WebSocket definitions work with dedicated plugins:
+
+```typescript
+import { initContract } from '@ts-contract/core';
+import { websocketPathPlugin, websocketValidatePlugin } from '@ts-contract/plugins';
+
+const api = initContract(contract)
+ .useWebSocket(websocketPathPlugin)
+ .useWebSocket(websocketValidatePlugin)
+ .build();
+
+// Build WebSocket URL
+const url = api.chat.buildPath({ roomId: '123' }, { token: 'abc' });
+// => "/ws/chat/123?token=abc"
+
+// Validate outgoing message
+const msg = api.chat.validateClientMessage('new_msg', {
+ type: 'new_msg',
+ body: 'Hello!',
+});
+
+// Validate incoming message
+const serverMsg = api.chat.validateServerMessage('new_msg', data);
+```
+
+## Type Inference
+
+Full TypeScript inference is available for all message types:
+
+```typescript
+import type {
+ InferClientMessage,
+ InferServerMessage,
+ InferWebSocketPathParams,
+ InferWebSocketQuery,
+} from '@ts-contract/core';
+
+// Infer specific message types
+type NewMsgClient = InferClientMessage;
+// => { type: 'new_msg', body: string }
+
+type NewMsgServer = InferServerMessage;
+// => { type: 'new_msg', id: string, body: string, userId: string }
+
+// Infer connection parameters
+type PathParams = InferWebSocketPathParams;
+// => { roomId: string }
+
+type Query = InferWebSocketQuery;
+// => { token: string }
+```
+
+## Framework Integration
+
+WebSocket contracts are designed to work with any WebSocket framework. The contract defines the message schemas and connection parameters, while your chosen framework handles the actual WebSocket connection lifecycle (open, close, error events).
+
+See the [Phoenix.js integration recipe](/recipes/websocket/phoenix-chat) for a complete example.
+
+## Connection Lifecycle
+
+ts-contract does **not** model connection lifecycle events (open, close, error). These are handled by your WebSocket framework:
+
+- **Phoenix.js**: `socket.onError()`, `socket.onClose()`, `channel.onClose()`
+- **Socket.io**: `socket.on('connect')`, `socket.on('disconnect')`
+- **Native WebSocket**: `ws.onopen`, `ws.onclose`, `ws.onerror`
+
+The contract focuses on message validation and type safety, not connection management.
+
+## Best Practices
+
+1. **Always include type discriminators** in message schemas
+2. **Group related WebSocket definitions** under a common namespace (e.g., `ws.chat`, `ws.notifications`)
+3. **Use descriptive event names** that match your backend implementation
+4. **Validate both directions** - client messages before sending, server messages on receipt
+5. **Keep message schemas focused** - one message type per event
+
+## Next Steps
+
+- Learn about [WebSocket plugins](/plugins/websocket-plugins)
+- See a [Phoenix.js integration example](/recipes/websocket/phoenix-chat)
+- Explore [type inference utilities](/api-reference/core/websocket-inference)
diff --git a/apps/docs/content/getting-started.mdx b/apps/docs/content/getting-started.mdx
index 42e249a..bf970b5 100644
--- a/apps/docs/content/getting-started.mdx
+++ b/apps/docs/content/getting-started.mdx
@@ -316,4 +316,228 @@ function UserProfile({ id }: { id: string }) {
);
}
-```
\ No newline at end of file
+```
+
+## Common Patterns
+
+### Creating a Complete CRUD API
+
+Here's a typical pattern for a resource with full CRUD operations:
+
+```ts
+import { createContract } from '@ts-contract/core';
+import { z } from 'zod';
+
+const UserSchema = z.object({
+ id: z.string(),
+ name: z.string(),
+ email: z.string().email(),
+});
+
+const contract = createContract({
+ users: {
+ list: {
+ method: 'GET',
+ path: '/users',
+ query: z.object({
+ page: z.string().optional(),
+ limit: z.string().optional(),
+ }),
+ responses: {
+ 200: z.object({
+ data: z.array(UserSchema),
+ total: z.number(),
+ }),
+ },
+ },
+ get: {
+ method: 'GET',
+ path: '/users/:id',
+ pathParams: z.object({ id: z.string() }),
+ responses: {
+ 200: UserSchema,
+ 404: z.object({ message: z.string() }),
+ },
+ },
+ create: {
+ method: 'POST',
+ path: '/users',
+ body: UserSchema.omit({ id: true }),
+ responses: {
+ 201: UserSchema,
+ 400: z.object({ message: z.string() }),
+ },
+ },
+ update: {
+ method: 'PUT',
+ path: '/users/:id',
+ pathParams: z.object({ id: z.string() }),
+ body: UserSchema.omit({ id: true }),
+ responses: {
+ 200: UserSchema,
+ 404: z.object({ message: z.string() }),
+ },
+ },
+ delete: {
+ method: 'DELETE',
+ path: '/users/:id',
+ pathParams: z.object({ id: z.string() }),
+ responses: {
+ 204: z.null(),
+ 404: z.object({ message: z.string() }),
+ },
+ },
+ },
+});
+```
+
+### Sharing Schemas Across Routes
+
+Extract common schemas to avoid duplication:
+
+```ts
+import { z } from 'zod';
+
+const ErrorSchema = z.object({ message: z.string() });
+const PaginationSchema = z.object({
+ page: z.string().optional(),
+ limit: z.string().optional(),
+});
+
+const contract = createContract({
+ getUsers: {
+ method: 'GET',
+ path: '/users',
+ query: PaginationSchema,
+ responses: {
+ 200: z.array(z.object({ id: z.string(), name: z.string() })),
+ 500: ErrorSchema,
+ },
+ },
+ getPosts: {
+ method: 'GET',
+ path: '/posts',
+ query: PaginationSchema,
+ responses: {
+ 200: z.array(z.object({ id: z.string(), title: z.string() })),
+ 500: ErrorSchema,
+ },
+ },
+});
+```
+
+### Organizing Large APIs
+
+Split your contract into multiple files for better organization:
+
+```ts
+// contracts/users.ts
+export const userRoutes = {
+ getUser: { /* ... */ },
+ createUser: { /* ... */ },
+};
+
+// contracts/posts.ts
+export const postRoutes = {
+ getPost: { /* ... */ },
+ createPost: { /* ... */ },
+};
+
+// contracts/index.ts
+import { createContract } from '@ts-contract/core';
+import { userRoutes } from './users';
+import { postRoutes } from './posts';
+
+export const contract = createContract({
+ users: userRoutes,
+ posts: postRoutes,
+});
+```
+
+## Troubleshooting
+
+### TypeScript Errors with Type Inference
+
+**Problem:** TypeScript can't infer types from your contract.
+
+**Solution:** Make sure you're using `typeof` when extracting types:
+
+```ts
+// ✓ Correct
+type User = InferResponseBody;
+
+// ✗ Wrong
+type User = InferResponseBody;
+```
+
+### Schema Validation Errors
+
+**Problem:** Validation fails with unexpected errors.
+
+**Solution:** Ensure your schema library is compatible with Standard Schema. ts-contract works with Zod, Valibot, Arktype, and any library implementing [@standard-schema/spec](https://github.com/standard-schema/standard-schema).
+
+### Plugin Methods Not Available
+
+**Problem:** `buildPath()` or `validateResponse()` methods don't exist.
+
+**Solution:** Make sure you've initialized the contract with plugins:
+
+```ts
+// ✓ Correct - plugins added
+const api = initContract(contract)
+ .use(pathPlugin)
+ .use(validatePlugin)
+ .build();
+
+// ✗ Wrong - no plugins
+const api = contract;
+```
+
+### Path Parameters Not Matching
+
+**Problem:** Path building fails with missing parameter errors.
+
+**Solution:** Ensure parameter names in `pathParams` match the placeholders in `path`:
+
+```ts
+// ✓ Correct - names match
+{
+ path: '/users/:userId/posts/:postId',
+ pathParams: z.object({
+ userId: z.string(),
+ postId: z.string(),
+ }),
+}
+
+// ✗ Wrong - names don't match
+{
+ path: '/users/:userId/posts/:postId',
+ pathParams: z.object({
+ id: z.string(),
+ post: z.string(),
+ }),
+}
+```
+
+## What's Next?
+
+Now that you've got the basics, dive deeper into ts-contract:
+
+### Core Concepts
+
+- **[Contracts](/core-concepts/contracts)** - Learn how to organize and compose contracts
+- **[Routes & Schemas](/core-concepts/routes-and-schemas)** - Deep dive into route definitions
+- **[Type Inference](/core-concepts/type-inference)** - Master type extraction from contracts
+- **[Plugin System](/core-concepts/plugin-system)** - Understand how plugins extend functionality
+
+### Plugins
+
+- **[Path Plugin](/plugins/path-plugin)** - Build type-safe URLs
+- **[Validate Plugin](/plugins/validate-plugin)** - Runtime schema validation
+- **[Creating Custom Plugins](/plugins/creating-custom-plugins)** - Build your own plugins
+
+### Recipes
+
+- **[Server Integrations](/recipes/server/hono)** - Integrate with Hono, Express, Fastify
+- **[Client Integrations](/recipes/client/react-query)** - Use with React Query, SWR, and more
+- **[Full-Stack Examples](/recipes/full-stack/monorepo)** - Complete end-to-end examples
\ No newline at end of file
diff --git a/apps/docs/content/guides/best-practices.mdx b/apps/docs/content/guides/best-practices.mdx
new file mode 100644
index 0000000..6002eca
--- /dev/null
+++ b/apps/docs/content/guides/best-practices.mdx
@@ -0,0 +1,890 @@
+---
+title: Best Practices
+description: Best practices and patterns for using ts-contract effectively in production applications.
+---
+
+## Contract Design
+
+### Use Shared Schemas
+
+Extract common schemas to avoid duplication:
+
+```ts
+import { z } from 'zod';
+
+// Shared schemas
+const UserSchema = z.object({
+ id: z.string(),
+ name: z.string(),
+ email: z.string().email(),
+});
+
+const ErrorSchema = z.object({
+ message: z.string(),
+});
+
+const PaginationSchema = z.object({
+ page: z.string().optional(),
+ limit: z.string().optional(),
+});
+
+// Use in contract
+const contract = createContract({
+ users: {
+ list: {
+ method: 'GET',
+ path: '/users',
+ query: PaginationSchema,
+ responses: {
+ 200: z.object({
+ users: z.array(UserSchema),
+ total: z.number(),
+ }),
+ 500: ErrorSchema,
+ },
+ },
+ get: {
+ method: 'GET',
+ path: '/users/:id',
+ pathParams: z.object({ id: z.string() }),
+ responses: {
+ 200: UserSchema,
+ 404: ErrorSchema,
+ },
+ },
+ },
+});
+```
+
+### Version Your APIs
+
+Include version in the path for breaking changes:
+
+```ts
+const contract = createContract({
+ v1: {
+ users: {
+ get: {
+ method: 'GET',
+ path: '/api/v1/users/:id',
+ // ...
+ },
+ },
+ },
+ v2: {
+ users: {
+ get: {
+ method: 'GET',
+ path: '/api/v2/users/:id',
+ // Different schema for v2
+ },
+ },
+ },
+});
+```
+
+### Document with Summaries
+
+Use the `summary` field for documentation:
+
+```ts
+const contract = createContract({
+ users: {
+ get: {
+ method: 'GET',
+ path: '/users/:id',
+ pathParams: z.object({ id: z.string() }),
+ responses: {
+ 200: UserSchema,
+ 404: ErrorSchema,
+ },
+ summary: 'Retrieve a user by their unique identifier',
+ },
+ },
+});
+```
+
+### Use Metadata for Custom Properties
+
+Store additional information in `metadata`:
+
+```ts
+const contract = createContract({
+ users: {
+ get: {
+ method: 'GET',
+ path: '/users/:id',
+ pathParams: z.object({ id: z.string() }),
+ responses: {
+ 200: UserSchema,
+ },
+ metadata: {
+ requiresAuth: true,
+ rateLimit: 100,
+ cacheTTL: 300,
+ tags: ['users', 'public'],
+ },
+ },
+ },
+});
+```
+
+## Type Safety
+
+### Always Use `typeof`
+
+When extracting types, always use `typeof`:
+
+```ts
+// ✓ Correct
+type User = InferResponseBody;
+
+// ✗ Wrong
+type User = InferResponseBody;
+```
+
+### Avoid `any` and Type Assertions
+
+Let TypeScript infer types from your contract:
+
+```ts
+// ✓ Good
+const user = api.users.get.validateResponse(200, data);
+// user is properly typed
+
+// ✗ Avoid
+const user = data as any;
+const user = data as User; // Only if absolutely necessary
+```
+
+### Use Type Guards for Unions
+
+Create type guards for discriminated unions:
+
+```ts
+type Response =
+ | { status: 200; body: User }
+ | { status: 404; body: { message: string } };
+
+function isSuccessResponse(res: Response): res is { status: 200; body: User } {
+ return res.status === 200;
+}
+
+if (isSuccessResponse(response)) {
+ console.log(response.body.name); // TypeScript knows this is User
+}
+```
+
+### Export Types from Contract Package
+
+In a monorepo, export types from your contract package:
+
+```ts
+// packages/contract/src/types.ts
+export type User = InferResponseBody;
+export type CreateUserBody = InferBody;
+export type UpdateUserBody = InferBody;
+
+// apps/api/src/routes/users.ts
+import type { User, CreateUserBody } from '@my-app/contract';
+```
+
+## Validation
+
+### Validate at Boundaries
+
+Always validate data at system boundaries:
+
+```ts
+// ✓ Good - validate incoming data
+app.post('/users', (req, res) => {
+ const body = api.users.create.validateBody(req.body);
+ const user = await database.createUser(body);
+ res.json(user);
+});
+
+// ✗ Bad - no validation
+app.post('/users', (req, res) => {
+ const user = await database.createUser(req.body);
+ res.json(user);
+});
+```
+
+### Validate API Responses
+
+Validate responses from external APIs:
+
+```ts
+// ✓ Good
+async function fetchUser(id: string) {
+ const response = await fetch(`/api/users/${id}`);
+ const data = await response.json();
+ return api.users.get.validateResponse(200, data);
+}
+
+// ✗ Bad - no validation
+async function fetchUser(id: string) {
+ const response = await fetch(`/api/users/${id}`);
+ return response.json();
+}
+```
+
+### Handle Validation Errors
+
+Provide meaningful error messages:
+
+```ts
+try {
+ const body = api.users.create.validateBody(req.body);
+} catch (error) {
+ if (error.name === 'ZodError') {
+ return res.status(400).json({
+ message: 'Validation failed',
+ errors: error.errors.map(e => ({
+ field: e.path.join('.'),
+ message: e.message,
+ })),
+ });
+ }
+ throw error;
+}
+```
+
+### Conditional Validation
+
+Skip validation in trusted environments:
+
+```ts
+const shouldValidate = process.env.NODE_ENV !== 'production';
+
+async function fetchUser(id: string) {
+ const response = await fetch(`/api/users/${id}`);
+ const data = await response.json();
+
+ return shouldValidate
+ ? api.users.get.validateResponse(200, data)
+ : data;
+}
+```
+
+## Plugin Usage
+
+### Initialize Once, Export
+
+Create your enhanced contract once:
+
+```ts
+// api.ts
+import { initContract } from '@ts-contract/core';
+import { pathPlugin, validatePlugin } from '@ts-contract/plugins';
+import { contract } from './contract';
+
+export const api = initContract(contract)
+ .use(pathPlugin)
+ .use(validatePlugin)
+ .build();
+```
+
+```ts
+// Other files
+import { api } from './api';
+```
+
+### Use Plugins Consistently
+
+Apply the same plugins across your application:
+
+```ts
+// ✓ Good - consistent
+const api = initContract(contract)
+ .use(pathPlugin)
+ .use(validatePlugin)
+ .build();
+
+// ✗ Avoid - inconsistent
+const clientApi = initContract(contract).use(pathPlugin).build();
+const serverApi = initContract(contract).use(validatePlugin).build();
+```
+
+### Choose Plugins Based on Needs
+
+**Client-side:**
+```ts
+// Client needs both path building and validation
+const api = initContract(contract)
+ .use(pathPlugin)
+ .use(validatePlugin)
+ .build();
+```
+
+**Server-side:**
+```ts
+// Server only needs validation (paths are defined by framework)
+const api = initContract(contract)
+ .use(validatePlugin)
+ .build();
+```
+
+## Error Handling
+
+### Use Consistent Error Responses
+
+Define standard error schemas:
+
+```ts
+const ErrorSchema = z.object({
+ message: z.string(),
+ code: z.string().optional(),
+ details: z.record(z.any()).optional(),
+});
+
+const contract = createContract({
+ users: {
+ get: {
+ method: 'GET',
+ path: '/users/:id',
+ pathParams: z.object({ id: z.string() }),
+ responses: {
+ 200: UserSchema,
+ 400: ErrorSchema,
+ 401: ErrorSchema,
+ 404: ErrorSchema,
+ 500: ErrorSchema,
+ },
+ },
+ },
+});
+```
+
+### Create Custom Error Classes
+
+```ts
+export class ApiError extends Error {
+ constructor(
+ message: string,
+ public status: number,
+ public code?: string,
+ public details?: Record
+ ) {
+ super(message);
+ this.name = 'ApiError';
+ }
+}
+
+// Usage
+throw new ApiError('User not found', 404, 'USER_NOT_FOUND');
+```
+
+### Handle Errors Gracefully
+
+```ts
+async function fetchUser(id: string) {
+ try {
+ const response = await fetch(`/api/users/${id}`);
+
+ if (!response.ok) {
+ if (response.status === 404) {
+ const error = await response.json();
+ throw new ApiError(error.message, 404);
+ }
+ throw new ApiError('Request failed', response.status);
+ }
+
+ const data = await response.json();
+ return api.users.get.validateResponse(200, data);
+ } catch (error) {
+ if (error instanceof ApiError) {
+ // Handle API errors
+ console.error('API Error:', error.message);
+ } else {
+ // Handle network errors
+ console.error('Network Error:', error);
+ }
+ throw error;
+ }
+}
+```
+
+## Performance
+
+### Minimize Bundle Size
+
+Only import what you need:
+
+```ts
+// ✓ Good - tree-shakeable
+import { createContract } from '@ts-contract/core';
+import { pathPlugin } from '@ts-contract/plugins';
+
+// ✗ Avoid - imports everything
+import * as tsContract from '@ts-contract/core';
+```
+
+
+### Conditional Validation
+
+Validate only in development or at boundaries:
+
+```ts
+const shouldValidate = process.env.NODE_ENV !== 'production';
+
+async function fetchUser(id: string) {
+ const response = await fetch(api.users.get.buildPath({ id }));
+ const data = await response.json();
+ return shouldValidate ? api.users.get.validateResponse(200, data) : data;
+}
+```
+
+### Skip Validation in Production
+
+For trusted internal APIs:
+
+```ts
+const api = process.env.NODE_ENV === 'production'
+ ? initContract(contract).use(pathPlugin).build()
+ : initContract(contract).use(pathPlugin).use(validatePlugin).build();
+```
+
+## WebSocket Contracts
+
+### Message Type Discriminators
+
+Always include type discriminators matching event names:
+
+```ts
+const contract = createContract({
+ chat: {
+ type: 'websocket',
+ path: '/ws/chat/:roomId',
+ pathParams: z.object({ roomId: z.string() }),
+ clientMessages: {
+ // ✓ Good - type matches event name
+ new_msg: z.object({
+ type: z.literal('new_msg'),
+ body: z.string(),
+ }),
+ // ✗ Avoid - missing type discriminator
+ typing: z.object({
+ isTyping: z.boolean(),
+ }),
+ },
+ serverMessages: {
+ new_msg: z.object({
+ type: z.literal('new_msg'),
+ id: z.string(),
+ body: z.string(),
+ }),
+ },
+ },
+});
+```
+
+### Organize by Channel/Topic
+
+Group WebSocket definitions by channel or topic:
+
+```ts
+const contract = createContract({
+ websockets: {
+ chat: { type: 'websocket', path: '/ws/chat/:roomId', /* ... */ },
+ notifications: { type: 'websocket', path: '/ws/notifications', /* ... */ },
+ presence: { type: 'websocket', path: '/ws/presence/:userId', /* ... */ },
+ },
+});
+```
+
+### Validate Messages at Boundaries
+
+Validate incoming and outgoing WebSocket messages:
+
+```ts
+// Validate incoming messages
+channel.on('new_msg', (data: unknown) => {
+ try {
+ const msg = api.chat.validateServerMessage('new_msg', data);
+ handleMessage(msg);
+ } catch (error) {
+ console.error('Invalid message:', error);
+ }
+});
+
+// Validate outgoing messages
+function sendMessage(body: string) {
+ const msg = api.chat.validateClientMessage('new_msg', {
+ type: 'new_msg',
+ body,
+ });
+ channel.push('new_msg', msg);
+}
+```
+
+## Testing
+
+### Test Contract Definitions
+
+```ts
+import { describe, it, expect } from 'vitest';
+import { contract } from './contract';
+
+describe('Contract', () => {
+ it('has correct structure', () => {
+ expect(contract.users.get.method).toBe('GET');
+ expect(contract.users.get.path).toBe('/users/:id');
+ });
+});
+```
+
+### Test Type Inference
+
+```ts
+import { describe, it, expectTypeOf } from 'vitest';
+import type { InferResponseBody } from '@ts-contract/core';
+import { contract } from './contract';
+
+describe('Type Inference', () => {
+ it('infers correct user type', () => {
+ type User = InferResponseBody;
+
+ expectTypeOf().toHaveProperty('id');
+ expectTypeOf().toHaveProperty('name');
+ expectTypeOf().toHaveProperty('email');
+ });
+});
+```
+
+### Test Validation
+
+```ts
+import { describe, it, expect } from 'vitest';
+import { api } from './api';
+
+describe('Validation', () => {
+ it('validates correct data', () => {
+ const validUser = {
+ id: '1',
+ name: 'Alice',
+ email: 'alice@example.com',
+ };
+
+ expect(() => {
+ api.users.get.validateResponse(200, validUser);
+ }).not.toThrow();
+ });
+
+ it('rejects invalid data', () => {
+ const invalidUser = {
+ id: '1',
+ name: 'Alice',
+ // Missing email
+ };
+
+ expect(() => {
+ api.users.get.validateResponse(200, invalidUser);
+ }).toThrow();
+ });
+});
+```
+
+### Mock API Responses
+
+```ts
+import { vi } from 'vitest';
+
+vi.mock('./api-client', () => ({
+ fetchUser: vi.fn().mockResolvedValue({
+ id: '1',
+ name: 'Alice',
+ email: 'alice@example.com',
+ }),
+}));
+```
+
+## Code Organization
+
+### Monorepo Structure
+
+```
+my-monorepo/
+├── packages/
+│ └── contract/
+│ ├── src/
+│ │ ├── index.ts
+│ │ ├── contract.ts
+│ │ ├── api.ts
+│ │ ├── types.ts
+│ │ └── schemas/
+│ │ ├── user.ts
+│ │ ├── post.ts
+│ │ └── common.ts
+│ └── package.json
+├── apps/
+│ ├── api/
+│ └── web/
+└── package.json
+```
+
+### Separate Concerns
+
+```ts
+// schemas/user.ts
+export const UserSchema = z.object({
+ id: z.string(),
+ name: z.string(),
+ email: z.string().email(),
+});
+
+// schemas/common.ts
+export const ErrorSchema = z.object({
+ message: z.string(),
+});
+
+export const PaginationSchema = z.object({
+ page: z.string().optional(),
+ limit: z.string().optional(),
+});
+
+// contract.ts
+import { UserSchema } from './schemas/user';
+import { ErrorSchema, PaginationSchema } from './schemas/common';
+
+export const contract = createContract({
+ users: {
+ list: {
+ query: PaginationSchema,
+ responses: {
+ 200: z.object({
+ users: z.array(UserSchema),
+ total: z.number(),
+ }),
+ 500: ErrorSchema,
+ },
+ },
+ },
+});
+```
+
+### Export Everything
+
+```ts
+// index.ts
+export { contract } from './contract';
+export { api } from './api';
+export * from './types';
+export * from './schemas/user';
+export * from './schemas/common';
+```
+
+## Security
+
+### Validate User Input
+
+Always validate and sanitize user input:
+
+```ts
+app.post('/users', (req, res) => {
+ try {
+ const body = api.users.create.validateBody(req.body);
+ // body is validated and sanitized
+ const user = await database.createUser(body);
+ res.json(user);
+ } catch (error) {
+ res.status(400).json({ message: 'Invalid input' });
+ }
+});
+```
+
+### Don't Expose Internal Errors
+
+```ts
+// ✓ Good
+app.use((err, req, res, next) => {
+ console.error(err); // Log internally
+ res.status(500).json({ message: 'Internal server error' });
+});
+
+// ✗ Bad - exposes stack traces
+app.use((err, req, res, next) => {
+ res.status(500).json({ message: err.message, stack: err.stack });
+});
+```
+
+### Use Environment Variables
+
+```ts
+const API_BASE_URL = process.env.API_URL || 'http://localhost:3000';
+const JWT_SECRET = process.env.JWT_SECRET;
+
+if (!JWT_SECRET) {
+ throw new Error('JWT_SECRET is required');
+}
+```
+
+### Rate Limit Endpoints
+
+```ts
+import rateLimit from 'express-rate-limit';
+
+const limiter = rateLimit({
+ windowMs: 15 * 60 * 1000,
+ max: 100,
+});
+
+app.use('/api/', limiter);
+```
+
+## Documentation
+
+### Document Your Contract
+
+Add comments to your contract:
+
+```ts
+/**
+ * User management API endpoints
+ */
+export const contract = createContract({
+ users: {
+ /**
+ * Retrieve a user by ID
+ * @requires Authentication
+ */
+ get: {
+ method: 'GET',
+ path: '/users/:id',
+ pathParams: z.object({ id: z.string() }),
+ responses: {
+ 200: UserSchema,
+ 404: ErrorSchema,
+ },
+ summary: 'Get user by ID',
+ },
+ },
+});
+```
+
+### Generate API Documentation
+
+Use metadata to generate docs:
+
+```ts
+function generateDocs(contract: any) {
+ for (const [key, route] of Object.entries(contract)) {
+ if ('method' in route) {
+ console.log(`${route.method} ${route.path}`);
+ console.log(`Summary: ${route.summary}`);
+ console.log(`Metadata:`, route.metadata);
+ }
+ }
+}
+```
+
+### Keep README Updated
+
+Document your contract package:
+
+```markdown
+# @my-app/contract
+
+Shared API contract for My App.
+
+## Installation
+
+\`\`\`bash
+pnpm add @my-app/contract
+\`\`\`
+
+## Usage
+
+\`\`\`ts
+import { api, type User } from '@my-app/contract';
+
+const user = await fetchUser('123');
+\`\`\`
+```
+
+## Deployment
+
+### Build Contract Package
+
+```json
+{
+ "scripts": {
+ "build": "tsc",
+ "prepublishOnly": "pnpm build"
+ }
+}
+```
+
+### Version Carefully
+
+Use semantic versioning for breaking changes (major), new features (minor), and bug fixes (patch).
+
+## Common Pitfalls
+
+### Don't Mix Concerns
+
+```ts
+// ✗ Bad - mixing business logic with contract
+const contract = createContract({
+ users: {
+ get: {
+ method: 'GET',
+ path: '/users/:id',
+ responses: {
+ 200: UserSchema,
+ },
+ // Don't add business logic here
+ handler: async (id) => database.findUser(id),
+ },
+ },
+});
+```
+
+### Don't Over-Validate
+
+```ts
+// ✗ Bad - validating trusted internal data
+function processUser(user: User) {
+ // Don't validate if you already know it's valid
+ const validated = api.users.get.validateResponse(200, user);
+ // ...
+}
+
+// ✓ Good - only validate at boundaries
+async function fetchUser(id: string) {
+ const response = await fetch(`/api/users/${id}`);
+ const data = await response.json();
+ return api.users.get.validateResponse(200, data);
+}
+```
+
+### Don't Ignore TypeScript Errors
+
+```ts
+// ✗ Bad - using @ts-ignore
+// @ts-ignore
+const user = api.users.get.buildPath({ id: 123 });
+
+// ✓ Good - fix the type error
+const user = api.users.get.buildPath({ id: '123' });
+```
+
+## Summary
+
+1. **Design contracts around domains**, not technical layers
+2. **Use shared schemas** to avoid duplication
+3. **Always validate at boundaries** (API responses, user input)
+4. **Export types** from your contract package
+5. **Initialize plugins once** and export
+6. **Handle errors gracefully** with consistent error schemas
+7. **Test your contracts** and type inference
+8. **Document your API** with summaries and metadata
+9. **Use semantic versioning** for breaking changes
+10. **Keep security in mind** - validate input, don't expose internals
+
+## Next Steps
+
+- Review [Contracts](/core-concepts/contracts) for organizing your API
+- See [WebSocket Contracts](/core-concepts/websocket-contracts) for real-time APIs
+- Explore [Type Inference](/core-concepts/type-inference) for extracting types
+- Check out [FAQ](/guides/faq) for common questions
diff --git a/apps/docs/content/guides/faq.mdx b/apps/docs/content/guides/faq.mdx
new file mode 100644
index 0000000..f07f585
--- /dev/null
+++ b/apps/docs/content/guides/faq.mdx
@@ -0,0 +1,675 @@
+---
+title: FAQ
+description: Frequently asked questions about ts-contract.
+---
+
+## General Questions
+
+### What is ts-contract?
+
+ts-contract is a schema-first TypeScript library for defining type-safe HTTP and WebSocket API contracts. It provides excellent TypeScript inference without code generation, allowing you to share types between frontend and backend.
+
+### Why use ts-contract instead of tRPC or similar tools?
+
+ts-contract is **minimal by design** with zero framework integrations. Unlike tRPC, which requires specific server and client implementations, ts-contract is just a contract definition that you integrate however you want. This makes it:
+
+- **Framework agnostic**: Works with any server (Express, Fastify, Hono, etc.)
+- **Client agnostic**: Works with any client (fetch, axios, React Query, etc.)
+- **Portable**: Easy to migrate between frameworks
+- **Lightweight**: Minimal bundle size
+
+### Does ts-contract work with JavaScript?
+
+ts-contract is designed for TypeScript. While it can technically work with JavaScript, you'll lose all the type safety benefits. We strongly recommend using TypeScript.
+
+### What schema libraries are supported?
+
+ts-contract supports any library that implements the [@standard-schema/spec](https://github.com/standard-schema/standard-schema) protocol:
+
+- **Zod** - Most popular, great DX
+- **Valibot** - Smaller bundle size, faster
+- **Arktype** - Fastest, unique syntax
+- Any custom Standard Schema implementation
+
+### Is ts-contract production-ready?
+
+Yes! ts-contract is stable and used in production applications. The API is stable and follows semantic versioning.
+
+## Setup & Installation
+
+### How do I install ts-contract?
+
+```bash
+pnpm add @ts-contract/core @ts-contract/plugins zod
+```
+
+You need:
+- `@ts-contract/core` - Core contract definitions and types
+- `@ts-contract/plugins` - Optional plugins (pathPlugin, validatePlugin)
+- A schema library (Zod, Valibot, or Arktype)
+
+### Do I need both core and plugins packages?
+
+- **`@ts-contract/core`** is required - it contains `createContract()`, `initContract()`, and type helpers
+- **`@ts-contract/plugins`** is optional - it provides `pathPlugin` and `validatePlugin`
+
+If you only need type inference without runtime utilities, you can skip the plugins package.
+
+### Can I use ts-contract in a monorepo?
+
+Yes! This is the recommended approach. Create a shared contract package that both your frontend and backend depend on.
+
+1. Create a `packages/contract` directory
+2. Define your contract in the package
+3. Add the contract package as a dependency to your apps
+4. Import and use the contract in both frontend and backend
+
+## Contract Definition
+
+### How do I define a contract?
+
+Use `createContract()` with your route definitions:
+
+```ts
+import { createContract } from '@ts-contract/core';
+import { z } from 'zod';
+
+const contract = createContract({
+ getUser: {
+ method: 'GET',
+ path: '/users/:id',
+ pathParams: z.object({ id: z.string() }),
+ responses: {
+ 200: z.object({ id: z.string(), name: z.string() }),
+ },
+ },
+});
+```
+
+### Can I nest contracts?
+
+Yes! You can organize routes into nested structures:
+
+```ts
+const contract = createContract({
+ users: {
+ list: { method: 'GET', path: '/users', /* ... */ },
+ get: { method: 'GET', path: '/users/:id', /* ... */ },
+ },
+ posts: {
+ list: { method: 'GET', path: '/posts', /* ... */ },
+ get: { method: 'GET', path: '/posts/:id', /* ... */ },
+ },
+});
+```
+
+### How do I handle multiple response types?
+
+Define multiple status codes in the `responses` object:
+
+```ts
+const contract = createContract({
+ getUser: {
+ method: 'GET',
+ path: '/users/:id',
+ pathParams: z.object({ id: z.string() }),
+ responses: {
+ 200: z.object({ id: z.string(), name: z.string() }),
+ 404: z.object({ message: z.string() }),
+ 500: z.object({ message: z.string() }),
+ },
+ },
+});
+```
+
+### Can I use the same schema for multiple routes?
+
+Yes! Extract schemas and reuse them:
+
+```ts
+const UserSchema = z.object({
+ id: z.string(),
+ name: z.string(),
+ email: z.string().email(),
+});
+
+const contract = createContract({
+ getUser: {
+ method: 'GET',
+ path: '/users/:id',
+ responses: { 200: UserSchema },
+ },
+ createUser: {
+ method: 'POST',
+ path: '/users',
+ body: UserSchema.omit({ id: true }),
+ responses: { 201: UserSchema },
+ },
+});
+```
+
+### How do I version my API?
+
+Include the version in the path:
+
+```ts
+const contract = createContract({
+ v1: {
+ users: {
+ get: {
+ method: 'GET',
+ path: '/api/v1/users/:id',
+ // ...
+ },
+ },
+ },
+ v2: {
+ users: {
+ get: {
+ method: 'GET',
+ path: '/api/v2/users/:id',
+ // Different schema
+ },
+ },
+ },
+});
+```
+
+## Type Inference
+
+### How do I extract types from my contract?
+
+Use the type helper utilities:
+
+```ts
+import type { InferResponseBody, InferBody, InferPathParams } from '@ts-contract/core';
+
+type User = InferResponseBody;
+type CreateUserBody = InferBody;
+type GetUserParams = InferPathParams;
+```
+
+### Why do I need to use `typeof`?
+
+TypeScript needs `typeof` to get the type of a value:
+
+```ts
+// ✓ Correct
+type User = InferResponseBody;
+
+// ✗ Wrong - contract.getUser is a value, not a type
+type User = InferResponseBody;
+```
+
+### Can I use inferred types in React components?
+
+Yes! This is one of the main benefits:
+
+```tsx
+import type { InferResponseBody } from '@ts-contract/core';
+import { contract } from './contract';
+
+type User = InferResponseBody;
+
+function UserProfile({ user }: { user: User }) {
+ return
{user.name}
;
+}
+```
+
+### How do I infer all possible responses?
+
+Use `InferResponses` for a discriminated union:
+
+```ts
+import type { InferResponses } from '@ts-contract/core';
+
+type Response = InferResponses;
+// => { status: 200; body: User } | { status: 404; body: { message: string } }
+```
+
+## Plugins
+
+### What are plugins?
+
+Plugins add runtime utilities to your contracts. The built-in plugins are:
+
+**HTTP Plugins:**
+- **pathPlugin** - Adds `buildPath()` for URL construction
+- **validatePlugin** - Adds validation methods
+
+**WebSocket Plugins:**
+- **websocketPathPlugin** - Adds `buildPath()` for WebSocket URLs
+- **websocketValidatePlugin** - Adds message validation methods
+
+### Do I need to use plugins?
+
+No! Plugins are optional. If you only need type inference, you can skip plugins:
+
+```ts
+// Without plugins - type inference only
+import type { InferResponseBody } from '@ts-contract/core';
+type User = InferResponseBody;
+
+// With plugins - runtime utilities
+import { initContract } from '@ts-contract/core';
+import { pathPlugin } from '@ts-contract/plugins';
+
+const api = initContract(contract).use(pathPlugin).build();
+const url = api.getUser.buildPath({ id: '123' });
+```
+
+### How do I use plugins?
+
+```ts
+const api = initContract(contract)
+ .use(pathPlugin)
+ .use(validatePlugin)
+ .build();
+```
+
+See [Plugin System](/core-concepts/plugin-system) for details.
+
+### Can I create custom plugins?
+
+Yes! See the [Creating Custom Plugins](/plugins/creating-custom-plugins) guide for details.
+
+### Should I use plugins on the server?
+
+Servers typically only need `validatePlugin` for request validation. Clients use both `pathPlugin` and `validatePlugin`.
+
+## WebSocket Questions
+
+### How do I define a WebSocket contract?
+
+Use `type: 'websocket'` and define client/server message schemas:
+
+```ts
+const contract = createContract({
+ chat: {
+ type: 'websocket',
+ path: '/ws/chat/:roomId',
+ pathParams: z.object({ roomId: z.string() }),
+ clientMessages: {
+ new_msg: z.object({
+ type: z.literal('new_msg'),
+ body: z.string(),
+ }),
+ },
+ serverMessages: {
+ new_msg: z.object({
+ type: z.literal('new_msg'),
+ id: z.string(),
+ body: z.string(),
+ }),
+ },
+ },
+});
+```
+
+### Do WebSocket messages need type discriminators?
+
+Yes! Each message schema should include a `type` field matching the event name:
+
+```ts
+clientMessages: {
+ // ✓ Good - type matches event name
+ new_msg: z.object({
+ type: z.literal('new_msg'),
+ body: z.string(),
+ }),
+}
+```
+
+### How do I use WebSocket plugins?
+
+Use `.useWebSocket()` instead of `.use()`:
+
+```ts
+import { websocketPathPlugin, websocketValidatePlugin } from '@ts-contract/plugins';
+
+const api = initContract(contract)
+ .useWebSocket(websocketPathPlugin)
+ .useWebSocket(websocketValidatePlugin)
+ .build();
+
+api.chat.buildPath({ roomId: '123' });
+api.chat.validateClientMessage('new_msg', data);
+```
+
+### Can I mix HTTP and WebSocket in one contract?
+
+Yes! You can have both HTTP routes and WebSocket definitions:
+
+```ts
+const contract = createContract({
+ http: {
+ getUser: { method: 'GET', path: '/users/:id', /* ... */ },
+ },
+ ws: {
+ chat: { type: 'websocket', path: '/ws/chat', /* ... */ },
+ },
+});
+```
+
+### What WebSocket frameworks are supported?
+
+ts-contract is framework-agnostic. It works with:
+- Native WebSocket API
+- Phoenix.js (Elixir)
+- Socket.io
+- Any WebSocket library
+
+See the [Phoenix.js Chat Recipe](/recipes/websocket/phoenix-chat) for an example.
+
+## Validation
+
+### When should I validate?
+
+Always validate at system boundaries:
+
+- **Server**: Validate incoming requests (body, params, query)
+- **Client**: Validate API responses
+- **External APIs**: Validate responses from third-party APIs
+
+### How do I validate request data?
+
+Use the `validatePlugin`:
+
+```ts
+const api = initContract(contract)
+ .use(validatePlugin)
+ .build();
+
+app.post('/users', (req, res) => {
+ try {
+ const body = api.createUser.validateBody(req.body);
+ // body is validated and typed
+ } catch (error) {
+ res.status(400).json({ message: error.message });
+ }
+});
+```
+
+### How do I validate API responses?
+
+```ts
+async function fetchUser(id: string) {
+ const response = await fetch(`/api/users/${id}`);
+ const data = await response.json();
+
+ // Validate and type the response
+ return api.getUser.validateResponse(200, data);
+}
+```
+
+### What happens if validation fails?
+
+The validation method throws an error with details about what failed:
+
+```ts
+try {
+ api.createUser.validateBody(invalidData);
+} catch (error) {
+ console.error(error.message);
+ // => "Validation failed for body of /users: ..."
+}
+```
+
+### Can I skip validation in production?
+
+Yes, for performance:
+
+```ts
+const shouldValidate = process.env.NODE_ENV !== 'production';
+
+const data = shouldValidate
+ ? api.getUser.validateResponse(200, rawData)
+ : rawData;
+```
+
+However, we recommend validating at least at system boundaries even in production.
+
+## Integration
+
+### How do I use ts-contract with Express?
+
+See the [Express Integration](/recipes/server/express) guide for a complete example.
+
+### How do I use ts-contract with React Query?
+
+See the [React Query Integration](/recipes/client/react-query) guide for a complete example.
+
+### Can I use ts-contract with REST clients like axios?
+
+Yes! ts-contract works with any HTTP client:
+
+```ts
+import axios from 'axios';
+
+async function fetchUser(id: string) {
+ const url = api.getUser.buildPath({ id });
+ const { data } = await axios.get(url);
+ return api.getUser.validateResponse(200, data);
+}
+```
+
+### Does ts-contract support WebSockets?
+
+The contract definition supports WebSocket routes, but the built-in plugins are focused on HTTP. You can create custom plugins for WebSocket-specific functionality.
+
+### Can I use ts-contract with GraphQL?
+
+ts-contract is designed for REST APIs. For GraphQL, consider using GraphQL's built-in type system or tools like GraphQL Code Generator.
+
+## Performance
+
+### What's the bundle size impact?
+
+- **@ts-contract/core**: ~2KB minified + gzipped
+- **pathPlugin**: ~500 bytes
+- **validatePlugin**: ~800 bytes (plus your schema library)
+
+The core library is very lightweight. Most of the bundle size comes from your schema library (Zod, Valibot, etc.).
+
+### Does validation impact performance?
+
+Yes, validation has a runtime cost:
+
+- **Zod**: ~10-50ms for typical schemas
+- **Valibot**: ~5-20ms (faster)
+- **Arktype**: ~5-15ms (fastest)
+
+For most applications, this is acceptable. For high-performance scenarios, consider:
+- Using faster schema libraries (Valibot, Arktype)
+- Validating only at boundaries
+- Skipping validation in production for trusted internal APIs
+
+### Should I use ts-contract in serverless functions?
+
+Yes! ts-contract works great in serverless environments. The small bundle size and zero dependencies (except your schema library) make it ideal for edge functions.
+
+## Troubleshooting
+
+### TypeScript can't infer my types
+
+Make sure you're using `typeof`:
+
+```ts
+// ✓ Correct
+type User = InferResponseBody;
+
+// ✗ Wrong
+type User = InferResponseBody;
+```
+
+### Plugin methods don't exist
+
+Make sure you called `.build()`:
+
+```ts
+// ✗ Wrong - forgot .build()
+const api = initContract(contract).use(pathPlugin);
+
+// ✓ Correct
+const api = initContract(contract).use(pathPlugin).build();
+```
+
+### Validation fails unexpectedly
+
+1. Check that your data matches the schema
+2. Log the actual data being validated
+3. Verify schema definitions
+4. Check for Date vs string conversions
+
+### Path parameters don't match
+
+Ensure parameter names in `pathParams` match the placeholders in `path`:
+
+```ts
+// ✓ Correct - names match
+{
+ path: '/users/:userId',
+ pathParams: z.object({ userId: z.string() }),
+}
+
+// ✗ Wrong - names don't match
+{
+ path: '/users/:userId',
+ pathParams: z.object({ id: z.string() }),
+}
+```
+
+### Contract changes don't reflect in my app
+
+In a monorepo, rebuild the contract package:
+
+```bash
+cd packages/contract
+pnpm build
+```
+
+Or use watch mode during development:
+
+```bash
+pnpm dev
+```
+
+## Best Practices
+
+### Should I validate on both client and server?
+
+Yes! Validate at all boundaries:
+
+- **Client**: Validate API responses to ensure type safety
+- **Server**: Validate incoming requests to prevent bad data
+
+### How should I organize my contracts?
+
+Organize by domain, not by HTTP method:
+
+```ts
+// ✓ Good - organized by domain
+const contract = createContract({
+ users: { list: {}, get: {}, create: {} },
+ posts: { list: {}, get: {}, create: {} },
+});
+
+// ✗ Avoid - organized by method
+const contract = createContract({
+ get: { users: {}, posts: {} },
+ post: { users: {}, posts: {} },
+});
+```
+
+### Should I use a monorepo?
+
+Yes, if you have both frontend and backend in the same repository. A monorepo allows you to:
+
+- Share the contract between apps
+- Get automatic type updates
+- Ensure frontend and backend stay in sync
+
+See the [Monorepo Setup](/recipes/full-stack/monorepo) guide.
+
+### How do I handle breaking changes?
+
+1. Use API versioning in your paths
+2. Follow semantic versioning for your contract package
+3. Communicate breaking changes to consumers
+4. Consider deprecation periods for major changes
+
+## Comparison with Other Tools
+
+### ts-contract vs tRPC
+
+**ts-contract:**
+- ✅ Framework agnostic
+- ✅ Minimal bundle size
+- ✅ Works with any client/server
+- ❌ Manual integration required
+
+**tRPC:**
+- ✅ Automatic client generation
+- ✅ Built-in React hooks
+- ❌ Requires specific server/client setup
+- ❌ Larger bundle size
+
+### ts-contract vs OpenAPI/Swagger
+
+**ts-contract:**
+- ✅ TypeScript-first
+- ✅ No code generation
+- ✅ Compile-time type safety
+- ❌ No automatic API documentation UI
+
+**OpenAPI:**
+- ✅ Language agnostic
+- ✅ Automatic documentation UI
+- ❌ Requires code generation for types
+- ❌ Runtime overhead
+
+### ts-contract vs Zod + manual types
+
+**ts-contract:**
+- ✅ Organized contract structure
+- ✅ Built-in type helpers
+- ✅ Plugin system
+- ✅ Consistent patterns
+
+**Zod alone:**
+- ✅ More flexible
+- ❌ More boilerplate
+- ❌ Manual type extraction
+- ❌ No standardized structure
+
+## Getting Help
+
+### Where can I get help?
+
+- **Documentation**: Check the [guides](/guides/best-practices) and [recipes](/recipes/server/hono)
+- **GitHub Issues**: Report bugs or request features
+- **Examples**: See the [recipes](/recipes/server/hono) for working examples
+
+### How do I report a bug?
+
+Open an issue on GitHub with:
+1. Minimal reproduction
+2. Expected behavior
+3. Actual behavior
+4. TypeScript and ts-contract versions
+
+### How do I request a feature?
+
+Open an issue on GitHub describing:
+1. The use case
+2. Why existing features don't work
+3. Proposed API (if you have ideas)
+
+## Next Steps
+
+- Check out [Getting Started](/getting-started) for a quick introduction
+- See [Contracts](/core-concepts/contracts) for organizing your API
+- Explore [WebSocket Contracts](/core-concepts/websocket-contracts) for real-time APIs
+- Review [Type Inference](/core-concepts/type-inference) for extracting types
+- Check [Best Practices](/guides/best-practices) for production tips
+- Set up a [Monorepo](/recipes/full-stack/monorepo) for your project
diff --git a/apps/docs/content/guides/meta.json b/apps/docs/content/guides/meta.json
new file mode 100644
index 0000000..4e8f776
--- /dev/null
+++ b/apps/docs/content/guides/meta.json
@@ -0,0 +1,4 @@
+{
+ "title": "Guides",
+ "pages": ["best-practices", "faq"]
+}
diff --git a/apps/docs/content/index.mdx b/apps/docs/content/index.mdx
index c33b4ea..3a18b2f 100644
--- a/apps/docs/content/index.mdx
+++ b/apps/docs/content/index.mdx
@@ -27,3 +27,44 @@ description: An opinionated schema-first TypeScript contract library for definin
Every integration adds surface area, complexity, and long-term maintenance burden.
ts-contract stays focused on a small, durable core with first-class TypeScript inference and composable primitives that make it easy to integrate with any stack.
+
+---
+
+## Contribute
+
+We welcome new ideas and contributions! Whether you want to report a bug, suggest a feature, or submit a pull request, your input helps make ts-contract better for everyone.
+
+