Skip to main content

API Client

The api global provides a full-featured HTTP client for making requests in your tests. Built on the modern Fetch API, it offers a clean, Promise-based interface with automatic JSON parsing, response timing, and full TypeScript support.

Overview

The API client is automatically available as a global api object in all your tests when using the API runner. It handles the complexities of HTTP communication so you can focus on writing tests.

FeatureDescription
All HTTP MethodsGET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
Automatic ParsingJSON responses are automatically parsed
Response TimingEvery response includes duration in milliseconds
Type SafetyFull TypeScript generics support
InterceptorsModify requests and responses globally
Retry LogicBuilt-in retry support with configurable delays

HTTP Methods

The client provides convenience methods for all standard HTTP methods:

GET Requests

Retrieve resources from the server.

// Simple GET request
const response = await api.get('/users')

// GET with type parameter for response data
const response = await api.get<User>('/users/1')
console.log(response.data.name) // TypeScript knows this is a User

// GET with query parameters
const response = await api.get('/users', {
params: { page: 1, limit: 10, sort: 'name' }
})
// Request URL: /users?page=1&limit=10&sort=name

POST Requests

Create new resources.

// POST with JSON body (automatically serialized)
const response = await api.post<User>('/users', {
name: 'John Doe',
email: 'john@example.com'
})
console.log(response.data.id) // ID of created user

// POST with custom headers
const response = await api.post('/users', userData, {
headers: { 'X-Request-Source': 'test-suite' }
})

PUT Requests

Replace an entire resource.

// PUT replaces the entire resource
const response = await api.put<User>('/users/1', {
name: 'Jane Doe',
email: 'jane@example.com',
role: 'admin'
})

PATCH Requests

Partially update a resource.

// PATCH updates only specified fields
const response = await api.patch<User>('/users/1', {
status: 'active'
})
// Only the status field is updated

DELETE Requests

Remove a resource.

// Simple DELETE
const response = await api.delete('/users/1')
expect(response.status).toBe(200) // or 204

// DELETE with confirmation body (some APIs require this)
const response = await api.delete('/users/1', {
body: { confirm: true }
})

HEAD Requests

Retrieve headers without the response body.

// Check if resource exists without downloading it
const response = await api.head('/files/large-file.zip')
const fileSize = response.headers.get('content-length')
console.log(`File size: ${fileSize} bytes`)

OPTIONS Requests

Check allowed methods and CORS headers.

// Check what methods are allowed
const response = await api.options('/users')
const allowedMethods = response.headers.get('allow')
console.log(`Allowed methods: ${allowedMethods}`)

Response Object

Every request returns an ApiResponse<T> object containing all information about the response:

interface ApiResponse<T> {
// HTTP status code (200, 201, 400, 404, 500, etc.)
status: number

// HTTP status text ("OK", "Created", "Not Found", etc.)
statusText: string

// Response headers (Headers object)
headers: Headers

// Parsed response body (type T)
data: T

// true if status is in 200-299 range
ok: boolean

// Request duration in milliseconds
duration: number

// Original request URL
url: string

// Request configuration used
request: RequestConfig
}

Working with Responses

const response = await api.get<User>('/users/1')

// Check status
if (response.ok) {
console.log('Request succeeded')
} else {
console.log(`Request failed: ${response.status} ${response.statusText}`)
}

// Access data
const user = response.data
console.log(`Hello, ${user.name}!`)

// Check headers
const contentType = response.headers.get('content-type')
const rateLimit = response.headers.get('x-rate-limit-remaining')

// Performance monitoring
if (response.duration > 1000) {
console.warn(`Slow request: ${response.duration}ms`)
}

Status Code Helpers

const response = await api.get('/users/1')

// Quick boolean checks
response.ok // true if 200-299
response.status // exact status code

// Common patterns
if (response.status === 404) {
console.log('User not found')
}

if (response.status >= 500) {
console.log('Server error occurred')
}

Request Options

Customize individual requests with options:

interface RequestOptions {
// Additional headers for this request
headers?: Record<string, string>

// Query parameters (appended to URL)
params?: Record<string, string | number | boolean>

// Request timeout in milliseconds
timeout?: number

// Number of retry attempts on failure
retries?: number

// Delay between retries in milliseconds
retryDelay?: number

// Request body (for POST, PUT, PATCH)
body?: unknown
}

Headers

const response = await api.get('/protected/resource', {
headers: {
'Authorization': 'Bearer my-token',
'X-Custom-Header': 'custom-value',
'Accept-Language': 'en-US'
}
})

Query Parameters

// Object-based (recommended)
const response = await api.get('/search', {
params: {
q: 'search term',
page: 1,
limit: 20,
includeArchived: false
}
})
// URL: /search?q=search%20term&page=1&limit=20&includeArchived=false

// Manual URL construction (also works)
const response = await api.get('/search?q=search%20term&page=1')

Timeout

// Per-request timeout
const response = await api.get('/slow-endpoint', {
timeout: 60000 // 60 seconds
})

// Different timeouts for different endpoints
const fastResponse = await api.get('/health', { timeout: 1000 })
const slowResponse = await api.get('/report/generate', { timeout: 300000 })

Retries

// Retry failed requests
const response = await api.get('/flaky-endpoint', {
retries: 3, // Try up to 3 times
retryDelay: 1000 // Wait 1 second between retries
})

Request Body Types

JSON (Default)

Objects are automatically serialized as JSON:

// Object body → Content-Type: application/json
const response = await api.post('/users', {
name: 'John',
email: 'john@example.com',
preferences: {
theme: 'dark',
notifications: true
}
})

FormData

For file uploads and multipart forms:

const formData = new FormData()
formData.append('file', new Blob(['file content']), 'document.txt')
formData.append('description', 'Test document')
formData.append('category', 'testing')

const response = await api.post('/files/upload', formData)
// Content-Type is automatically set to multipart/form-data

URL Encoded

For traditional form submissions:

const params = new URLSearchParams()
params.append('username', 'john')
params.append('password', 'secret123')
params.append('remember', 'true')

const response = await api.post('/auth/login', params)
// Content-Type: application/x-www-form-urlencoded

Raw String

For plain text or custom formats:

const response = await api.post('/log', 'Plain text log message', {
headers: { 'Content-Type': 'text/plain' }
})

// XML example
const xmlBody = `<?xml version="1.0"?>
<user>
<name>John</name>
<email>john@example.com</email>
</user>`

const response = await api.post('/users', xmlBody, {
headers: { 'Content-Type': 'application/xml' }
})

Dynamic Configuration

Modify client settings during test execution:

Base URL Management

// Get current base URL
const currentUrl = api.getBaseUrl()
console.log(`Testing against: ${currentUrl}`)

// Change base URL mid-test (useful for multi-service testing)
api.setBaseUrl('https://service-a.example.com')
const responseA = await api.get('/endpoint')

api.setBaseUrl('https://service-b.example.com')
const responseB = await api.get('/endpoint')

// Reset to original
api.setBaseUrl('https://api.example.com')

Header Management

// Add or update a single header
api.setHeader('Authorization', 'Bearer new-token')
api.setHeader('X-Request-ID', crypto.randomUUID())

// Add or update multiple headers at once
api.setHeaders({
'X-Request-ID': crypto.randomUUID(),
'X-Client-Version': '2.0.0',
'X-Environment': 'staging'
})

// Remove a header
api.removeHeader('X-Temporary-Header')

// Get all current default headers
const headers = api.getHeaders()
console.log('Current headers:', headers)

Example: Token Refresh

describe('User API with token refresh', () => {
let accessToken: string

before(async () => {
// Login and get initial token
const loginResponse = await api.post('/auth/login', {
username: 'testuser',
password: 'testpass'
})
accessToken = loginResponse.data.accessToken
api.setHeader('Authorization', `Bearer ${accessToken}`)
})

it('should refresh token when expired', async () => {
// Simulate token expiration by getting a new one
const refreshResponse = await api.post('/auth/refresh')
accessToken = refreshResponse.data.accessToken
api.setHeader('Authorization', `Bearer ${accessToken}`)

// Continue with refreshed token
const response = await api.get('/protected/resource')
expect(response.status).toBe(200)
})
})

Custom Request

For full control over the request, use the request method:

const response = await api.request('/custom-endpoint', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Custom-Header': 'value'
},
body: JSON.stringify({
query: 'custom query',
options: { format: 'json' }
}),
timeout: 30000
})

GraphQL via Custom Request

const response = await api.request('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`,
variables: { id: '123' }
})
})

const user = response.data.data.user

Type Safety

Use TypeScript generics for fully typed responses:

Define Response Types

// Define your API response types
interface User {
id: number
name: string
email: string
createdAt: string
}

interface UserListResponse {
users: User[]
total: number
page: number
pageSize: number
}

interface CreateUserRequest {
name: string
email: string
password: string
}

interface ApiError {
code: string
message: string
details?: Record<string, string[]>
}

Type-Safe Requests

// GET with typed response
const response = await api.get<User>('/users/1')
const userName: string = response.data.name // TypeScript knows this is string

// POST with typed response
const newUser: CreateUserRequest = {
name: 'John',
email: 'john@example.com',
password: 'secure123'
}
const createResponse = await api.post<User>('/users', newUser)
const userId: number = createResponse.data.id

// List with pagination
const listResponse = await api.get<UserListResponse>('/users', {
params: { page: 1, pageSize: 10 }
})
const users: User[] = listResponse.data.users
const total: number = listResponse.data.total

Error Type Handling

const response = await api.post<User>('/users', userData)

if (response.ok) {
// Success response
const user: User = response.data
console.log(`Created user: ${user.name}`)
} else {
// Error response - cast to error type
const error = response.data as ApiError
console.log(`Error: ${error.message}`)
if (error.details) {
Object.entries(error.details).forEach(([field, messages]) => {
console.log(` ${field}: ${messages.join(', ')}`)
})
}
}

Best Practices

1. Use Type Parameters

Always specify response types for better IDE support and compile-time checking:

// Good
const response = await api.get<User>('/users/1')

// Avoid
const response = await api.get('/users/1')

2. Handle Errors Gracefully

Check response.ok before accessing data:

const response = await api.get<User>('/users/1')

if (!response.ok) {
throw new Error(`Failed to fetch user: ${response.status}`)
}

// Safe to use response.data here
const user = response.data

3. Use Interceptors for Cross-Cutting Concerns

Don't repeat authentication logic in every request:

// Good: Set up once in before hook
before(async () => {
api.addRequestInterceptor(authInterceptor)
})

// Avoid: Passing token in every request
const response = await api.get('/users', {
headers: { 'Authorization': `Bearer ${token}` }
})

4. Configure Timeouts Appropriately

Set reasonable timeouts based on expected response times:

// Health checks should be fast
await api.get('/health', { timeout: 2000 })

// Report generation might take longer
await api.get('/reports/annual', { timeout: 120000 })

Next Steps