# Typed-Fetch: Comprehensive AI Documentation
## Overview
**@pbpeterson/typed-fetch** is a TypeScript-first HTTP client that implements Go-style error handling, where "errors are values, not exceptions." It's a thin, type-safe wrapper around the native fetch API that never throws and provides comprehensive error handling for all HTTP status codes.
**Key Philosophy**: Instead of using try-catch blocks and exceptions for control flow, typed-fetch returns errors as regular values that can be inspected, handled, and passed around like any other data.
## Core Features
### 1. Never Throws
- All errors are returned as values, never thrown as exceptions
- Eliminates unexpected crashes from unhandled HTTP errors
- Predictable error handling patterns
### 2. Fully Type-Safe
- Complete TypeScript support for requests, responses, and errors
- Generic types for response bodies and expected error types
- TypeScript knows which properties are available based on success/error state
### 3. Comprehensive Error System
- **40+ specific HTTP error classes** covering all standard status codes (400-599)
- **Network error handling** separate from HTTP errors
- **Static properties** for accessing status codes without instantiation
- **Response body parsing** methods (json, text, blob, arrayBuffer) on all error objects
- **Error cloning** support for multiple response body reads
### 4. Enhanced TypedHeaders
- IntelliSense support for common HTTP headers
- Type-safe header values with common patterns
- Case-insensitive header support
- Covers Content-Type, Authorization, CORS, Security headers, etc.
### 5. Type-Safe HTTP Methods
- `HttpMethods` type for all standard HTTP methods
- Full TypeScript autocomplete for method values
- Compile-time method validation
- Supports GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, CONNECT, TRACE
### 6. Tree-Shaking Friendly
- Individual error class exports for optimal bundle size
- **Always import only the specific errors you handle**
- Built for modern bundlers to eliminate unused code
## Installation
```bash
npm install @pbpeterson/typed-fetch
# or
pnpm add @pbpeterson/typed-fetch
# or
yarn add @pbpeterson/typed-fetch
```
## Basic Usage
### Simple GET Request
```typescript
import { typedFetch } from '@pbpeterson/typed-fetch';
interface User {
id: number;
name: string;
email: string;
}
const { response, error } = await typedFetch<User[]>('/api/users');
if (error) {
// Handle error - TypeScript knows error is not null
console.error('Request failed:', error.statusText);
if ('json' in error) {
const errorDetails = await error.json();
console.error('Error details:', errorDetails);
}
} else {
// Handle success - TypeScript knows response is not null
const users = await response.json(); // Type: User[]
console.log('Users:', users);
}
```
### POST Request with Typed Headers
```typescript
import { typedFetch, BadRequestError, UnprocessableEntityError } from '@pbpeterson/typed-fetch';
const newUser = { name: 'John Doe', email: 'john@example.com' };
const { response, error } = await typedFetch<User>('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json', // TypeScript provides autocomplete
'Authorization': 'Bearer your-token', // Type-checked format
'Accept': 'application/json',
},
body: JSON.stringify(newUser),
});
if (error) {
if (error instanceof BadRequestError) {
const validationErrors = await error.json();
console.error('Validation failed:', validationErrors);
} else if (error instanceof UnprocessableEntityError) {
const entityErrors = await error.json();
console.error('Entity processing failed:', entityErrors);
} else {
console.error('Request failed:', error.statusText);
}
} else {
const user = await response.json(); // Type: User
console.log('Created user:', user);
}
```
## Error Handling System
### Error Type Hierarchy
```typescript
// Base classes
BaseHttpError (abstract)
├── ClientErrors (4xx)
│ ├── BadRequestError (400)
│ ├── UnauthorizedError (401)
│ ├── PaymentRequiredError (402)
│ ├── ForbiddenError (403)
│ ├── NotFoundError (404)
│ ├── MethodNotAllowedError (405)
│ ├── NotAcceptableError (406)
│ ├── ProxyAuthenticationRequiredError (407)
│ ├── RequestTimeoutError (408)
│ ├── ConflictError (409)
│ ├── GoneError (410)
│ ├── LengthRequiredError (411)
│ ├── PreconditionFailedError (412)
│ ├── RequestTooLongError (413)
│ ├── RequestUriTooLongError (414)
│ ├── UnsupportedMediaTypeError (415)
│ ├── RequestedRangeNotSatisfiableError (416)
│ ├── ExpectationFailedError (417)
│ ├── ImATeapotError (418)
│ ├── MisdirectedRequestError (421)
│ ├── UnprocessableEntityError (422)
│ ├── LockedError (423)
│ ├── FailedDependencyError (424)
│ ├── TooEarlyError (425)
│ ├── UpgradeRequiredError (426)
│ ├── PreconditionRequiredError (428)
│ ├── TooManyRequestsError (429)
│ ├── RequestHeaderFieldsTooLargeError (431)
│ └── UnavailableForLegalReasonsError (451)
└── ServerErrors (5xx)
├── InternalServerError (500)
├── NotImplementedError (501)
├── BadGatewayError (502)
├── ServiceUnavailableError (503)
├── GatewayTimeoutError (504)
├── HttpVersionNotSupportedError (505)
├── VariantAlsoNegotiatesError (506)
├── InsufficientStorageError (507)
├── LoopDetectedError (508)
├── NotExtendedError (510)
└── NetworkAuthenticationRequiredError (511)
// Network errors (separate from HTTP errors)
NetworkError
```
### Error Properties and Methods
All HTTP error classes provide:
**Instance Properties:**
- `status: number` - HTTP status code
- `statusText: string` - HTTP status message
- `headers: Headers` - Response headers object
**Instance Methods:**
- `json(): Promise<any>` - Parse error response body as JSON
- `text(): Promise<string>` - Parse error response body as text
- `blob(): Promise<Blob>` - Parse error response body as blob
- `arrayBuffer(): Promise<ArrayBuffer>` - Parse error response body as ArrayBuffer
- `clone(): ErrorClass` - Clone the error for multiple body reads
**Static Properties:**
- `static status: number` - Access status code without instantiation
- `static statusText: string` - Access status text without instantiation
### Specific Error Type Handling
```typescript
import {
typedFetch,
BadRequestError,
NotFoundError,
UnauthorizedError,
NetworkError
} from '@pbpeterson/typed-fetch';
// Constrain expected client errors (server errors always included)
type ExpectedErrors = BadRequestError | NotFoundError;
const { response, error } = await typedFetch<User, ExpectedErrors>('/api/users/123');
if (error) {
// error type: BadRequestError | NotFoundError | ServerErrors | NetworkError
if (error instanceof BadRequestError) {
// Handle validation errors
const details = await error.json();
console.log('Validation failed:', details);
} else if (error instanceof NotFoundError) {
// Handle resource not found
console.log('User not found');
} else if (error instanceof UnauthorizedError) {
// Handle authentication issues
console.log('Please authenticate');
} else if (error instanceof NetworkError) {
// Handle network connectivity issues
console.log('Network error:', error.message);
} else {
// Handle server errors (5xx)
console.log('Server error:', error.status, error.statusText);
}
}
```
### Static Error Properties
```typescript
import { NotFoundError, BadRequestError, InternalServerError } from '@pbpeterson/typed-fetch';
// Check status codes without creating instances
console.log(NotFoundError.status); // 404
console.log(NotFoundError.statusText); // "Not Found"
console.log(BadRequestError.status); // 400
console.log(InternalServerError.status); // 500
// Use in conditional logic
if (someResponse.status === NotFoundError.status) {
console.log('Resource not found');
}
```
## TypedHeaders System
### Enhanced Header Types
The `TypedHeaders` interface provides comprehensive TypeScript support for HTTP headers with intelligent autocomplete:
```typescript
import { typedFetch } from '@pbpeterson/typed-fetch';
const { response, error } = await typedFetch<Data>('/api/endpoint', {
method: 'POST',
headers: {
// Content headers with autocomplete
'Content-Type': 'application/json', // Suggests common MIME types
'Content-Encoding': 'gzip', // Suggests encoding types
'Content-Length': '1024', // Template literal pattern
// Authentication headers
'Authorization': 'Bearer token123', // Suggests Bearer/Basic patterns
'WWW-Authenticate': 'Bearer realm="api"',
// Content negotiation
'Accept': 'application/json', // Suggests content types + */*
'Accept-Encoding': 'gzip, deflate', // Suggests encoding preferences
'Accept-Language': 'en-US, en', // Suggests language codes
// Caching headers
'Cache-Control': 'no-cache', // Suggests cache directives
'ETag': '"abc123"', // Template literal for quotes
'If-None-Match': '"abc123"', // ETag format checking
'If-Modified-Since': 'Wed, 21 Oct 2015 07:28:00 GMT',
// CORS headers
'Access-Control-Allow-Origin': '*', // Suggests wildcard or specific origins
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Allow-Credentials': 'true', // Boolean string values
// Security headers
'Content-Security-Policy': "default-src 'self'",
'X-Frame-Options': 'DENY', // Suggests DENY, SAMEORIGIN, ALLOW-FROM
'X-Content-Type-Options': 'nosniff',
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
// Connection and transfer
'Connection': 'keep-alive', // Suggests connection types
'Range': 'bytes=0-1023', // Template literal for byte ranges
'User-Agent': 'MyApp/1.0',
'Referer': 'https://example.com',
'Origin': 'https://mysite.com',
// Custom headers also supported
'X-Custom-Header': 'custom-value',
}
});
```
### Header Type Features
1. **Autocomplete Support**: Common header values are suggested by TypeScript
2. **Template Literals**: Complex patterns like `Bearer ${string}` or `bytes=${string}`
3. **String Union + Custom**: Uses `(string & {})` pattern for best of both worlds
4. **Case-Insensitive**: Canonical type mapping supports different case variations
5. **Extensible**: Index signature allows custom headers while maintaining type safety
## HttpMethods Type
### Type-Safe HTTP Methods
The `HttpMethods` type provides TypeScript support for all standard HTTP methods:
```typescript
import { typedFetch, HttpMethods } from '@pbpeterson/typed-fetch';
// Type-safe method specification
const method: HttpMethods = 'POST'; // ✅ Valid
const invalidMethod: HttpMethods = 'INVALID'; // ❌ TypeScript error
const { response, error } = await typedFetch<User>('/api/users', {
method: 'POST', // TypeScript autocomplete available
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData),
});
```
### Supported HTTP Methods
```typescript
export type HttpMethods =
| "GET"
| "POST"
| "PUT"
| "PATCH"
| "DELETE"
| "HEAD"
| "OPTIONS"
| "CONNECT"
| "TRACE";
```
### Method Type Features
1. **Standard Compliance**: Covers all HTTP/1.1 standard methods
2. **TypeScript Integration**: Full autocomplete and type checking
3. **Case Sensitive**: Methods must be uppercase (following HTTP specification)
4. **IDE Support**: IntelliSense suggestions for all valid methods
## Advanced Usage Patterns
### Response Body Parsing
```typescript
const { response, error } = await typedFetch<ApiResponse>('/api/data');
if (error) {
// Parse error response in multiple formats
try {
const errorJson = await error.json();
console.log('Structured error:', errorJson);
} catch {
const errorText = await error.clone().text();
console.log('Text error:', errorText);
}
} else {
// Parse successful response
const data = await response.json(); // Type: ApiResponse
const text = await response.clone().text();
const blob = await response.clone().blob();
}
```
### Error Cloning for Multiple Reads
```typescript
const { response, error } = await typedFetch<Data>('/api/endpoint');
if (error) {
// Clone error to read body multiple times
const error1 = error.clone();
const error2 = error.clone();
const jsonData = await error1.json();
const textData = await error2.text();
console.log('JSON:', jsonData);
console.log('Text:', textData);
}
```
### Network vs HTTP Error Distinction
```typescript
import { typedFetch, NetworkError } from '@pbpeterson/typed-fetch';
const { response, error } = await typedFetch<Data>('/api/endpoint');
if (error) {
if (error instanceof NetworkError) {
// Handle connectivity issues, timeouts, DNS failures
console.log('Network problem:', error.message);
// No status code available - pure network issue
} else {
// Handle HTTP errors (got response from server)
console.log(`HTTP error: ${error.status} ${error.statusText}`);
const errorBody = await error.json();
}
}
```
### Combining Multiple Error Types
```typescript
import {
typedFetch,
BadRequestError,
UnauthorizedError,
NotFoundError
} from '@pbpeterson/typed-fetch';
type UserErrors = BadRequestError | UnauthorizedError | NotFoundError;
const { response, error } = await typedFetch<User, UserErrors>('/api/users/123');
if (error) {
// TypeScript knows: UserErrors | ServerErrors | NetworkError
// Handle specific client errors
if (error instanceof BadRequestError) {
console.log('Invalid request data');
} else if (error instanceof UnauthorizedError) {
console.log('Authentication required');
} else if (error instanceof NotFoundError) {
console.log('User not found');
}
// Server errors and network errors also handled automatically
}
```
## API Reference
### `typedFetch<T, E>(url, options?)`
**Type Parameters:**
- `T` - Expected response body type
- `E extends ClientErrors` - Specific client error types to expect (optional)
**Parameters:**
- `url: string` - Request URL
- `options: RequestInit & { headers?: TypedHeaders; method?: HttpMethods }` - Fetch options (optional)
**Returns:**
```typescript
Promise<{
response: TypedResponse<T>;
error: null;
} | {
response: null;
error: E | ServerErrors | NetworkError;
}>
```
**Note**: Server errors (5xx) are always included in the error union because they can occur regardless of client-side validation.
### TypedResponse<T>
Extends the standard `Response` interface with typed JSON parsing:
```typescript
interface TypedResponse<T> extends Response {
json(): Promise<T>; // Returns typed data instead of any
}
```
### Exports
All types and error classes are individually exported for tree-shaking:
```typescript
// Recommended: Import function, types, and only needed errors
import {
typedFetch,
HttpMethods,
TypedHeaders,
BadRequestError,
NotFoundError,
InternalServerError,
NetworkError
} from '@pbpeterson/typed-fetch';
// This approach optimizes bundle size by only including the errors you handle
```
## Best Practices
### 1. Always Check Errors First
```typescript
const { response, error } = await typedFetch<Data>('/api/data');
if (error) {
// Handle error case first
return handleError(error);
}
// TypeScript now knows response is not null
const data = await response.json();
```
### 2. Use Specific Error Types
```typescript
// Good: Specify expected client errors
const { response, error } = await typedFetch<User, BadRequestError | NotFoundError>('/api/users/123');
// Less specific: All client errors included
const { response, error } = await typedFetch<User>('/api/users/123');
```
### 3. Handle Network vs HTTP Errors
```typescript
if (error) {
if (error instanceof NetworkError) {
// Retry logic, offline handling, etc.
return handleNetworkError(error);
} else {
// HTTP error with status code
return handleHttpError(error);
}
}
```
### 4. Use Error Cloning for Multiple Reads
```typescript
if (error) {
try {
const jsonError = await error.json();
handleStructuredError(jsonError);
} catch {
const textError = await error.clone().text();
handleTextError(textError);
}
}
```
### 5. Leverage Static Properties
```typescript
import { NotFoundError, BadRequestError } from '@pbpeterson/typed-fetch';
// Use static properties for constants
const ERROR_CODES = {
NOT_FOUND: NotFoundError.status, // 404
BAD_REQUEST: BadRequestError.status, // 400
} as const;
// Use in switch statements
switch (error.status) {
case NotFoundError.status:
return handleNotFound();
case BadRequestError.status:
return handleBadRequest();
}
```
## Comparison with Traditional Approaches
### Traditional fetch with try-catch
```typescript
// ❌ Traditional approach - mixed error handling
try {
const response = await fetch('/api/users');
if (!response.ok) {
// Manual status code checking
if (response.status === 404) {
throw new Error('Not found');
} else if (response.status === 400) {
throw new Error('Bad request');
}
// Easy to miss status codes
throw new Error('Request failed');
}
const users = await response.json(); // What if this throws?
return users;
} catch (error) {
// Network errors, parsing errors, HTTP errors all mixed
console.error('Something went wrong:', error);
}
```
### typed-fetch approach
```typescript
// ✅ typed-fetch - explicit and type-safe
const { response, error } = await typedFetch<User[]>('/api/users');
if (error) {
if (error instanceof NotFoundError) {
console.log('Users not found');
} else if (error instanceof BadRequestError) {
const details = await error.json();
console.log('Validation errors:', details);
} else if (error instanceof NetworkError) {
console.log('Network issue:', error.message);
} else {
console.log(`Server error: ${error.status}`);
}
return;
}
// TypeScript guarantees response is available
const users = await response.json(); // Type: User[]
```
## Integration Examples
### With React Hooks
```typescript
import { useState, useEffect } from 'react';
import { typedFetch, NotFoundError, NetworkError } from '@pbpeterson/typed-fetch';
function useUser(userId: string) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function fetchUser() {
const { response, error } = await typedFetch<User>(`/api/users/${userId}`);
if (error) {
if (error instanceof NotFoundError) {
setError('User not found');
} else if (error instanceof NetworkError) {
setError('Connection failed');
} else {
setError(`Error: ${error.statusText}`);
}
} else {
const userData = await response.json();
setUser(userData);
}
setLoading(false);
}
fetchUser();
}, [userId]);
return { user, loading, error };
}
```
### With Error Boundary Integration
```typescript
import { typedFetch, NetworkError } from '@pbpeterson/typed-fetch';
class APIService {
async getUser(id: string): Promise<User> {
const { response, error } = await typedFetch<User>(`/api/users/${id}`);
if (error) {
if (error instanceof NetworkError) {
// Don't crash the app for network issues
throw new RecoverableError(error.message);
} else {
// HTTP errors can be handled gracefully
throw new APIError(error.status, error.statusText);
}
}
return response.json();
}
}
```
## Testing with typed-fetch
```typescript
import { describe, test, expect, vi } from 'vitest';
import { typedFetch, BadRequestError, NetworkError } from '@pbpeterson/typed-fetch';
describe('API Service', () => {
test('should handle successful response', async () => {
// Mock successful response
global.fetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: () => Promise.resolve({ id: 1, name: 'John' }),
});
const { response, error } = await typedFetch<User>('/api/users/1');
expect(error).toBe(null);
expect(response).not.toBe(null);
const user = await response!.json();
expect(user.name).toBe('John');
});
test('should handle 400 error with details', async () => {
const errorDetails = { field: 'email', message: 'Invalid format' };
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 400,
statusText: 'Bad Request',
headers: new Headers(),
json: () => Promise.resolve(errorDetails),
clone: () => ({ json: () => Promise.resolve(errorDetails) }),
});
const { response, error } = await typedFetch<User>('/api/users');
expect(response).toBe(null);
expect(error).toBeInstanceOf(BadRequestError);
if (error instanceof BadRequestError) {
const details = await error.json();
expect(details.field).toBe('email');
}
});
test('should handle network errors', async () => {
global.fetch = vi.fn().mockRejectedValue(new TypeError('Failed to fetch'));
const { response, error } = await typedFetch<User>('/api/users');
expect(response).toBe(null);
expect(error).toBeInstanceOf(NetworkError);
expect(error?.message).toBe('Failed to fetch');
});
});
```
## Bundle Size Optimization
typed-fetch is designed for optimal tree-shaking. **Always import only the specific errors you handle:**
```typescript
// ✅ RECOMMENDED - only imports what you need
import { typedFetch, BadRequestError, NotFoundError } from '@pbpeterson/typed-fetch';
// ❌ AVOID - imports everything, increases bundle size
import * as TypedFetch from '@pbpeterson/typed-fetch';
```
Each error class is a separate module, so bundlers can eliminate unused error types from your final bundle.
## Migration Guide
### From fetch
```typescript
// Before
try {
const response = await fetch('/api/data');
if (!response.ok) throw new Error('Request failed');
const data = await response.json();
} catch (error) {
console.error(error);
}
// After
const { response, error } = await typedFetch<DataType>('/api/data');
if (error) {
console.error(error.statusText);
return;
}
const data = await response.json();
```
### From axios
```typescript
// Before
try {
const response = await axios.get<DataType>('/api/data');
const data = response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
console.error(error.response?.status);
}
}
// After
const { response, error } = await typedFetch<DataType>('/api/data');
if (error) {
console.error(error.status);
return;
}
const data = await response.json();
```
## Conclusion
@pbpeterson/typed-fetch provides a modern, type-safe approach to HTTP requests that eliminates the unpredictability of exception-based error handling. By treating errors as values rather than exceptions, it enables more robust, maintainable code with excellent TypeScript integration and comprehensive error handling capabilities.
The library's design philosophy of "never throw" combined with extensive type safety makes it an excellent choice for TypeScript applications that prioritize reliability and developer experience.