Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ build/Release
node_modules/
jspm_packages/

# Lock files (project uses pnpm)
package-lock.json
yarn.lock

# Snowpack dependency directory (https://snowpack.dev/)
web_modules/

Expand Down
66 changes: 62 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
- **🎯 Type-Safe**: Full TypeScript support with strict type inference
- **📊 Standard Schema**: Native support for Zod, Valibot, and other Standard Schema compliant libraries
- **🔍 Request/Response Validation**: Runtime validation with detailed error messages
- **💡 Suggestions Only Mode**: Skip validation for performance while keeping type safety
- **🏗️ Builder Pattern**: Intuitive API inspired by Hono and Octokit
- **📋 Structured Response**: Rich response metadata (headers, status, URL) with `~raw` access
- **⚡ Lightweight**: Zero dependencies (except peer dependencies)
Expand Down Expand Up @@ -262,6 +263,7 @@ interface TypeFetcherConfig {
readonly headers?: Record<string, string>;
readonly timeout?: number;
readonly fetch?: typeof globalThis.fetch; // Custom fetch implementation
readonly skipValidation?: boolean; // Skip validation globally (schemas still provide type inference)
}
```

Expand All @@ -285,10 +287,11 @@ Registers a new endpoint with optional schema validation.
**Schema Object:**
```typescript
interface EndpointSchema {
readonly params?: StandardSchemaV1; // Path parameters
readonly query?: StandardSchemaV1; // Query parameters
readonly body?: StandardSchemaV1; // Request body
readonly response?: StandardSchemaV1; // Response validation
readonly params?: StandardSchemaV1; // Path parameters
readonly query?: StandardSchemaV1; // Query parameters
readonly body?: StandardSchemaV1; // Request body
readonly response?: StandardSchemaV1; // Response validation
readonly skipValidation?: boolean; // Skip validation for this endpoint (overrides global setting)
}
```

Expand Down Expand Up @@ -485,6 +488,61 @@ const response = await api.request("GET /items/{id}", {
});
```

### Suggestions Only Mode (Skip Validation)

For production environments where validation performance is critical or where API response changes shouldn't break the application, you can skip runtime validation while still maintaining TypeScript type safety:

```typescript
// Skip validation globally - schemas still provide type inference
const client = new TypeFetcher({
baseURL: "https://api.example.com",
skipValidation: true // All endpoints skip validation by default
});

const api = client.addEndpoint("GET", "/users/{id}", {
response: z.object({
id: z.number(),
name: z.string(),
})
});

// No runtime validation, but response.data is still typed as { id: number; name: string }
const response = await api.request("GET /users/{id}", {
params: { id: "123" }
});
```

You can also skip validation per-endpoint (overrides global setting):

```typescript
const client = new TypeFetcher({
baseURL: "https://api.example.com",
skipValidation: false // Validation enabled by default
});

const api = client
.addEndpoint("GET", "/users", {
response: z.array(UserSchema),
skipValidation: true // Skip validation for this endpoint only
})
.addEndpoint("POST", "/users", {
body: CreateUserSchema,
response: UserSchema
// This endpoint will validate because global default is false
});
```

**Benefits:**
- **Performance**: Skip validation overhead in production
- **Resilience**: Avoid errors when API responses change unexpectedly
- **Type Safety**: TypeScript types are still inferred from schemas
- **Flexibility**: Configure globally or per-endpoint

**Use Cases:**
- High-performance production APIs where validation is done server-side
- Legacy APIs with evolving schemas
- Development environments where you want types but not strict validation

## 🌟 Why TypeFetcher?

### Standard Schema Native
Expand Down
279 changes: 279 additions & 0 deletions examples/skip-validation-usage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
/**
* TypeFetcher Skip Validation (Suggestions Only) Usage Examples
*
* This file demonstrates how to use schemas for type inference only,
* without performing runtime validation. This is useful for:
* - Performance-critical production environments
* - APIs where validation is handled server-side
* - Legacy APIs with evolving schemas
*/

import { TypeFetcher } from "../dist";
import { z } from "zod";

/**
* Global skip validation example
* All endpoints will skip validation by default
*/
function globalSkipValidationExample() {
// Define schemas for type inference
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});

const CreateUserSchema = z.object({
name: z.string(),
email: z.string().email(),
});

// Create client with global skipValidation
const client = new TypeFetcher({
baseURL: "https://jsonplaceholder.typicode.com",
skipValidation: true, // Skip validation for all endpoints
});

// Register endpoints with schemas
// Schemas provide TypeScript types but no runtime validation
const api = client
.addEndpoint("GET", "/users", {
response: z.array(UserSchema),
})
.addEndpoint("GET", "/users/{id}", {
params: z.object({ id: z.string() }),
response: UserSchema,
})
.addEndpoint("POST", "/users", {
body: CreateUserSchema,
response: UserSchema,
});

return {
async getUsers() {
// Response is typed as User[] but not validated at runtime
const response = await api.request("GET /users");
// response.data has type User[] from schema
return response.data;
},

async getUser(id: string) {
// Params are typed but not validated at runtime
const response = await api.request("GET /users/{id}", {
params: { id }, // Type-safe but no validation
});
return response.data;
},

async createUser(userData: { name: string; email: string }) {
// Body is typed but not validated at runtime
const response = await api.request("POST /users", {
body: userData, // Type-safe but no validation
});
return response.data;
},
};
}

/**
* Per-endpoint skip validation example
* Fine-grained control over which endpoints skip validation
*/
function perEndpointSkipValidationExample() {
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});

// Create client with validation enabled by default
const client = new TypeFetcher({
baseURL: "https://api.example.com",
skipValidation: false, // Default: validate
});

const api = client
.addEndpoint("GET", "/users", {
response: z.array(UserSchema),
skipValidation: true, // Skip validation for this endpoint
})
.addEndpoint("POST", "/users", {
body: z.object({
name: z.string(),
email: z.string().email(),
}),
response: UserSchema,
// This endpoint WILL validate (uses global default: false)
});

return {
async getUsers() {
// No validation - even if response doesn't match schema
const response = await api.request("GET /users");
return response.data;
},

async createUser(userData: { name: string; email: string }) {
// This WILL validate - throws ValidationError if invalid
const response = await api.request("POST /users", {
body: userData,
});
return response.data;
},
};
}

/**
* Mixed validation example
* Use validation for critical endpoints, skip for others
*/
function mixedValidationExample() {
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});

const client = new TypeFetcher({
baseURL: "https://api.example.com",
skipValidation: true, // Skip validation by default
});

const api = client
// Analytics endpoint - skip validation for performance
.addEndpoint("POST", "/analytics/track", {
body: z.object({
event: z.string(),
data: z.record(z.unknown()),
}),
skipValidation: true,
})
// User creation - validate for data integrity
.addEndpoint("POST", "/users", {
body: z.object({
name: z.string().min(1),
email: z.string().email(),
}),
response: UserSchema,
skipValidation: false, // Override global setting
})
// Profile fetch - skip validation for performance
.addEndpoint("GET", "/profile", {
response: UserSchema,
// Uses global default: skipValidation: true
});

return {
async trackEvent(event: string, data: Record<string, unknown>) {
// High-performance, no validation overhead
await api.request("POST /analytics/track", {
body: { event, data },
});
},

async createUser(name: string, email: string) {
// Validated for data integrity
const response = await api.request("POST /users", {
body: { name, email },
});
return response.data;
},

async getProfile() {
// Fast fetch without validation
const response = await api.request("GET /profile");
return response.data;
},
};
}

/**
* Production environment example
* Skip validation in production, enable in development
*/
function environmentBasedValidationExample() {
const isProd = process.env.NODE_ENV === "production";

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

// Skip validation in production for performance
// Enable validation in development for debugging
const client = new TypeFetcher({
baseURL: process.env.API_BASE_URL || "https://api.example.com",
skipValidation: isProd, // Skip in prod, validate in dev
});

const api = client
.addEndpoint("GET", "/users", {
response: z.array(UserSchema),
})
.addEndpoint("GET", "/users/{id}", {
params: z.object({ id: z.string() }),
response: UserSchema,
});

console.log(
isProd ? "Running in production mode (validation skipped)" : "Running in development mode (validation enabled)"
);

return api;
}

/**
* Legacy API example
* Skip validation for evolving API responses
*/
function legacyApiExample() {
// Define the "ideal" schema, but skip validation
// because the legacy API might return extra or missing fields
const LegacyUserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().optional(), // Email might not always be present
// Legacy API might return other unexpected fields
});

const client = new TypeFetcher({
baseURL: "https://legacy-api.example.com",
skipValidation: true, // Don't break on unexpected fields
});

const api = client.addEndpoint("GET", "/users/{id}", {
params: z.object({ id: z.string() }),
response: LegacyUserSchema,
});

return {
async getUser(id: string) {
// Won't throw error even if response has extra fields
// or missing optional fields
const response = await api.request("GET /users/{id}", {
params: { id },
});

// Still get TypeScript type safety
const user = response.data;
console.log(`User ID: ${user.id}, Name: ${user.name}`);

// Handle optional fields safely
if (user.email) {
console.log(`Email: ${user.email}`);
}

return user;
},
};
}

// Export usage examples
export {
globalSkipValidationExample,
perEndpointSkipValidationExample,
mixedValidationExample,
environmentBasedValidationExample,
legacyApiExample,
};
Loading