Overview
RoZod provides comprehensive type safety through Zod schema validation and TypeScript type inference. All endpoints, parameters, and responses are fully typed at compile time and validated at runtime.
Endpoint type inference
RoZod automatically infers TypeScript types from Zod schemas:
import { fetchApi } from 'rozod' ;
import { getUsersUserdetails } from 'rozod/lib/endpoints/usersv1' ;
const response = await fetchApi ( getUsersUserdetails , {
userIds: [ 1 , 123456 ]
});
if ( ! isAnyErrorResponse ( response )) {
// TypeScript knows exact response structure
response . data [ 0 ]. id ; // number
response . data [ 0 ]. name ; // string
response . data [ 0 ]. displayName ; // string
response . data [ 0 ]. hasVerifiedBadge ; // boolean
}
No manual type annotations needed. Types are inferred from the endpoint definition.
Parameter validation
Parameters are validated against Zod schemas:
import { fetchApi } from 'rozod' ;
import { getGamesIcons } from 'rozod/lib/endpoints/gamesv1' ;
// TypeScript error: universeIds must be number[]
const response = await fetchApi ( getGamesIcons , {
universeIds: [ '123' , '456' ] // ❌ Error: Type 'string' is not assignable to type 'number'
});
// Correct
const response = await fetchApi ( getGamesIcons , {
universeIds: [ 123 , 456 ] // ✅ Valid
});
Required vs optional parameters
TypeScript enforces required parameters:
import { z } from 'zod' ;
import { endpoint , fetchApi } from 'rozod' ;
const myEndpoint = endpoint ({
method: 'GET' ,
path: '/v1/items' ,
baseUrl: 'https://api.example.com' ,
parameters: {
userId: z . number (). int (), // Required
limit: z . number (). default ( 10 ), // Optional (has default)
filter: z . string (). optional (), // Optional
},
response: z . object ({ data: z . array ( z . any ()) }),
});
// TypeScript error: userId is required
await fetchApi ( myEndpoint , {}); // ❌
// Valid: userId provided, optional params omitted
await fetchApi ( myEndpoint , { userId: 123 }); // ✅
// Valid: all parameters provided
await fetchApi ( myEndpoint , { // ✅
userId: 123 ,
limit: 25 ,
filter: 'active'
});
Default values
Parameters with defaults are optional but have guaranteed values:
import { z } from 'zod' ;
import { endpoint } from 'rozod' ;
const getItems = endpoint ({
method: 'GET' ,
path: '/v1/items' ,
baseUrl: 'https://api.example.com' ,
parameters: {
sortOrder: z . enum ([ 'Asc' , 'Desc' ]). default ( 'Desc' ),
limit: z . number (). default ( 25 ),
},
response: z . object ({ data: z . array ( z . any ()) }),
});
// Uses defaults: sortOrder='Desc', limit=25
await fetchApi ( getItems , {});
// Override defaults
await fetchApi ( getItems , { sortOrder: 'Asc' , limit: 50 });
Response type inference
Response types are inferred from endpoint schemas:
import { fetchApi } from 'rozod' ;
import { getGamesIcons } from 'rozod/lib/endpoints/gamesv1' ;
const response = await fetchApi ( getGamesIcons , {
universeIds: [ 1534453623 ]
});
if ( ! isAnyErrorResponse ( response )) {
// Inferred type:
// {
// data: Array<{
// targetId: number;
// state: string;
// imageUrl: string;
// }>;
// }
response . data . forEach ( icon => {
console . log ( icon . targetId ); // number
console . log ( icon . imageUrl ); // string
});
}
Union types (default error handling)
By default, responses are typed as SuccessType | AnyError:
import { fetchApi , isAnyErrorResponse } from 'rozod' ;
import { getUsersUserdetails } from 'rozod/lib/endpoints/usersv1' ;
// Type: UserDetailsResponse | AnyError
const response = await fetchApi ( getUsersUserdetails , {
userIds: [ 123456 ]
});
// Type guard narrows to UserDetailsResponse
if ( isAnyErrorResponse ( response )) {
// TypeScript knows: response is AnyError
console . error ( response . message );
return ;
}
// TypeScript knows: response is UserDetailsResponse
console . log ( response . data [ 0 ]. name );
Throw-on-error types
With throwOnError: true, responses are typed as only the success type:
import { fetchApi } from 'rozod' ;
import { getUsersUserdetails } from 'rozod/lib/endpoints/usersv1' ;
try {
// Type: UserDetailsResponse (no AnyError union)
const response = await fetchApi (
getUsersUserdetails ,
{ userIds: [ 123456 ] },
{ throwOnError: true }
);
// No error checking needed - type is always success
console . log ( response . data [ 0 ]. name );
} catch ( error ) {
console . error (( error as Error ). message );
}
Raw response types
With returnRaw: true, get a typed Response object:
import { fetchApi } from 'rozod' ;
import { getUsersUserdetails } from 'rozod/lib/endpoints/usersv1' ;
// Type: Response with typed json() method
const response = await fetchApi (
getUsersUserdetails ,
{ userIds: [ 123456 ] },
{ returnRaw: true }
);
// json() returns the properly typed response
const data = await response . json ();
// Type: { data: Array<{ id: number; name: string; ... }> }
Runtime validation
Zod validates all data at runtime, catching API changes early:
import { z } from 'zod' ;
import { endpoint , fetchApi } from 'rozod' ;
const myEndpoint = endpoint ({
method: 'GET' ,
path: '/v1/user/:userId' ,
baseUrl: 'https://api.example.com' ,
parameters: {
userId: z . number (). int (). positive (),
},
response: z . object ({
id: z . number (),
email: z . string (). email (),
age: z . number (). min ( 0 ). max ( 120 ),
}),
});
// Runtime validation catches invalid data
const response = await fetchApi ( myEndpoint , {
userId: 123
});
// If API returns invalid data, Zod will throw
// Example: email field is not a valid email
// Example: age is negative or > 120
If the API response doesn’t match the Zod schema, Zod will throw a validation error. This protects against unexpected API changes.
Custom endpoint types
Define custom endpoints with full type safety:
import { z } from 'zod' ;
import { endpoint , fetchApi } from 'rozod' ;
// Define schemas
const UserSchema = z . object ({
id: z . number (),
name: z . string (),
email: z . string (). email (),
role: z . enum ([ 'admin' , 'user' , 'guest' ]),
createdAt: z . string (). datetime (),
});
const PaginatedResponseSchema = z . object ({
data: z . array ( UserSchema ),
nextPageCursor: z . string (). nullable (),
previousPageCursor: z . string (). nullable (),
});
// Create endpoint
const getUsers = endpoint ({
method: 'GET' ,
path: '/v1/users' ,
baseUrl: 'https://api.example.com' ,
parameters: {
cursor: z . string (). optional (),
limit: z . number (). min ( 1 ). max ( 100 ). default ( 25 ),
},
response: PaginatedResponseSchema ,
});
// Fully typed usage
const response = await fetchApi ( getUsers , { limit: 50 });
if ( ! isAnyErrorResponse ( response )) {
// TypeScript knows exact types
response . data . forEach ( user => {
console . log ( user . id ); // number
console . log ( user . role ); // 'admin' | 'user' | 'guest'
});
const cursor = response . nextPageCursor ; // string | null
}
Zod schema patterns
Enums and literals
import { z } from 'zod' ;
// Enum values
const status = z . enum ([ 'pending' , 'active' , 'archived' ]);
// Single literal
const method = z . literal ( 'GET' );
// Union of literals
const method = z . union ([
z . literal ( 'GET' ),
z . literal ( 'POST' ),
z . literal ( 'PUT' ),
]);
Nested objects
import { z } from 'zod' ;
const UserSchema = z . object ({
id: z . number (),
profile: z . object ({
displayName: z . string (),
bio: z . string (). optional (),
avatar: z . object ({
url: z . string (). url (),
width: z . number (),
height: z . number (),
}),
}),
});
Arrays and records
import { z } from 'zod' ;
// Array of specific type
const numbers = z . array ( z . number ());
// Array with min/max length
const ids = z . array ( z . number ()). min ( 1 ). max ( 100 );
// Record/dictionary
const metadata = z . record ( z . string (), z . any ());
// Specific record keys
const config = z . object ({
settings: z . record ( z . string (), z . boolean ()),
});
import { z } from 'zod' ;
// Parse string to number
const stringToNumber = z . string (). transform ( val => parseInt ( val , 10 ));
// Date parsing
const dateSchema = z . string (). transform ( val => new Date ( val ));
// Custom transformation
const upperCase = z . string (). transform ( val => val . toUpperCase ());
Refinements
import { z } from 'zod' ;
// Custom validation
const password = z . string (). refine (
val => val . length >= 8 ,
{ message: 'Password must be at least 8 characters' }
);
// Multiple refinements
const userId = z . number ()
. int ()
. positive ()
. refine ( val => val <= 2147483647 , {
message: 'User ID exceeds maximum value'
});
RoZod provides utilities to extract types from endpoints:
import { type ExtractParams , type ExtractResponse } from 'rozod' ;
import { getUsersUserdetails } from 'rozod/lib/endpoints/usersv1' ;
// Extract parameter type
type Params = ExtractParams < typeof getUsersUserdetails >;
// { userIds: number[] }
// Extract response type
type Response = ExtractResponse < typeof getUsersUserdetails >;
// { data: Array<{ id: number; name: string; ... }> }
// Use in function signatures
function processUsers ( params : Params ) : void {
// params.userIds is correctly typed
}
function displayUsers ( response : Response ) : void {
// response.data is correctly typed
}
Working with generic types
Endpoints use generic types internally for flexibility:
import { type EndpointGeneric } from 'rozod' ;
// Type signature of endpoint definitions
type MyEndpoint = EndpointGeneric <
{ userId : number }, // Parameters
{ id : number ; name : string }, // Response
undefined // Body (optional)
>;
You rarely need to work with generic types directly. Use ExtractParams and ExtractResponse instead.
Benefits of type safety
Compile-time validation
Catch errors before runtime:
// TypeScript catches this immediately
await fetchApi ( endpoint , {
userId: '123' // ❌ Error: Expected number, got string
});
// Correct
await fetchApi ( endpoint , {
userId: 123 // ✅
});
Autocomplete
IDEs provide intelligent autocomplete:
const response = await fetchApi ( getUsersUserdetails , {
userIds: [ 123 ]
});
if ( ! isAnyErrorResponse ( response )) {
response . data [ 0 ]. // IDE shows: id, name, displayName, hasVerifiedBadge, etc.
}
Refactoring safety
Changing schemas updates types everywhere:
// Change schema
const endpoint = endpoint ({
// ... other config
response: z . object ({
userId: z . number (), // Renamed from 'id'
userName: z . string (), // Renamed from 'name'
}),
});
// TypeScript flags all uses of old property names
response . id // ❌ Error: Property 'id' does not exist
response . userId // ✅ Correct
API contract enforcement
Zod validation ensures APIs return expected data:
// If API changes response structure, Zod catches it
const response = await fetchApi ( endpoint , params );
// Zod validates response matches schema
// Throws if API returns unexpected data
Next steps
Endpoints Learn how to define and use typed API endpoints.
Error handling Understand error handling patterns with type safety.