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
6 changes: 3 additions & 3 deletions docs/generators/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export class NatsClient {

### HTTP

For `http` a single API client class is generated that wraps the standalone [`http_client`](../protocols/http_client.md) channel functions. You construct it once with the shared request configuration (`server`, `auth`, `hooks`, retry, pagination, ...) and every operation becomes a method that reuses that configuration; any field can still be overridden per call.
For `http` a single API client class is generated that wraps the standalone [`http_client`](../protocols/http_client.md) channel functions. You construct it once with the shared request configuration (`baseUrl`, `auth`, `hooks`, retry, ...) and every operation becomes a method that reuses that configuration; any field can still be overridden per call.

```js
export default {
Expand All @@ -140,7 +140,7 @@ Example generated usage:
import {SafepayNordicClient} from './__gen__/client/SafepayNordicClient';

const safepay = new SafepayNordicClient({
server: 'https://api.example.com',
baseUrl: 'https://api.example.com',
auth: {type: 'bearer', token: process.env.API_TOKEN ?? ''}
});

Expand All @@ -149,4 +149,4 @@ const response = await safepay.getV2Documents();
console.log(response.status, response.data);
```

Each method returns the same `HttpClientResponse<T>` as the underlying channel function, so response metadata (status, headers, pagination helpers) is preserved.
Each method returns the same `HttpClientResponse<T>` as the underlying channel function, so response metadata (status, headers) is preserved.
115 changes: 18 additions & 97 deletions docs/protocols/http_client.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ sidebar_position: 99

# HTTP(S)

HTTP client generator creates type-safe functions for making HTTP requests based on your API specification. It supports various authentication methods, pagination, retry logic, and extensibility hooks.
HTTP client generator creates type-safe functions for making HTTP requests based on your API specification. It supports various authentication methods, retry logic, and extensibility hooks.

It is currently available through the generators ([channels](../generators/channels.md)):

Expand All @@ -16,10 +16,6 @@ This is available through [AsyncAPI](../inputs/asyncapi.md) ([requires the HTTP
|---|---|
| Download | ❌ |
| Upload | ❌ |
| Offset based Pagination | ✅ |
| Cursor based Pagination | ✅ |
| Page based Pagination | ✅ |
| Range based Pagination | ✅ |
| Retry with backoff | ✅ |
| OAuth2 Authorization code | ❌ (browser-only) |
| OAuth2 Implicit | ❌ (browser-only) |
Expand Down Expand Up @@ -118,7 +114,7 @@ const pingMessage = new Ping({ message: 'Hello!' });
// Make a simple request
const response = await postPingPostRequest({
payload: pingMessage,
server: 'https://api.example.com'
baseUrl: 'https://api.example.com'
});

// Access the response
Expand Down Expand Up @@ -147,14 +143,14 @@ import { GetV2ConnectReferenceIdParameters } from './__gen__/channels/parameter/

// Request with a body: build the model, pass it as `payload`.
const created = await http_client.postV2Connect({
server: 'https://api.example.com',
baseUrl: 'https://api.example.com',
payload: new PostV2ConnectRequest({ returnUrl: 'https://shop.example/return' })
});
console.log(created.data.connectUrl); // typed response model

// Request with a path parameter: supply it through the parameter model.
const connect = await http_client.getV2ConnectReferenceId({
server: 'https://api.example.com',
baseUrl: 'https://api.example.com',
parameters: new GetV2ConnectReferenceIdParameters({ referenceId: 'ref_123' })
});
console.log(connect.data.safepayAccountId);
Expand All @@ -171,7 +167,7 @@ The HTTP client uses a discriminated union for authentication, providing excelle
```typescript
const response = await postPingPostRequest({
payload: message,
server: 'https://api.example.com',
baseUrl: 'https://api.example.com',
auth: {
type: 'bearer',
token: 'your-jwt-token'
Expand All @@ -184,7 +180,7 @@ const response = await postPingPostRequest({
```typescript
const response = await postPingPostRequest({
payload: message,
server: 'https://api.example.com',
baseUrl: 'https://api.example.com',
auth: {
type: 'basic',
username: 'user',
Expand All @@ -199,7 +195,7 @@ const response = await postPingPostRequest({
// API Key in header (default)
const response = await postPingPostRequest({
payload: message,
server: 'https://api.example.com',
baseUrl: 'https://api.example.com',
auth: {
type: 'apiKey',
key: 'your-api-key',
Expand All @@ -211,7 +207,7 @@ const response = await postPingPostRequest({
// API Key in query parameter
const response = await postPingPostRequest({
payload: message,
server: 'https://api.example.com',
baseUrl: 'https://api.example.com',
auth: {
type: 'apiKey',
key: 'your-api-key',
Expand All @@ -228,7 +224,7 @@ For server-to-server authentication:
```typescript
const response = await postPingPostRequest({
payload: message,
server: 'https://api.example.com',
baseUrl: 'https://api.example.com',
auth: {
type: 'oauth2',
flow: 'client_credentials',
Expand All @@ -251,7 +247,7 @@ For legacy applications requiring username/password:
```typescript
const response = await postPingPostRequest({
payload: message,
server: 'https://api.example.com',
baseUrl: 'https://api.example.com',
auth: {
type: 'oauth2',
flow: 'password',
Expand All @@ -277,7 +273,7 @@ const accessToken = getTokenFromBrowserFlow();

const response = await postPingPostRequest({
payload: message,
server: 'https://api.example.com',
baseUrl: 'https://api.example.com',
auth: {
type: 'oauth2',
accessToken: accessToken,
Expand All @@ -292,89 +288,14 @@ const response = await postPingPostRequest({
});
```

## Pagination

The HTTP client supports multiple pagination strategies. Pagination parameters can be placed in query parameters or headers.

### Offset-based Pagination

```typescript
const response = await getItemsRequest({
server: 'https://api.example.com',
pagination: {
type: 'offset',
offset: 0,
limit: 25,
in: 'query', // 'query' or 'header'
offsetParam: 'offset', // Query param name (default: 'offset')
limitParam: 'limit' // Query param name (default: 'limit')
}
});

// Navigate pages
if (response.hasNextPage?.()) {
const nextPage = await response.getNextPage?.();
}
```

### Cursor-based Pagination

```typescript
const response = await getItemsRequest({
server: 'https://api.example.com',
pagination: {
type: 'cursor',
cursor: undefined, // First page
limit: 25,
cursorParam: 'cursor'
}
});

// Get next page using cursor from response
if (response.pagination?.nextCursor) {
const nextPage = await response.getNextPage?.();
}
```

### Page-based Pagination

```typescript
const response = await getItemsRequest({
server: 'https://api.example.com',
pagination: {
type: 'page',
page: 1,
pageSize: 25,
pageParam: 'page',
pageSizeParam: 'per_page'
}
});
```

### Range-based Pagination (RFC 7233)

```typescript
const response = await getItemsRequest({
server: 'https://api.example.com',
pagination: {
type: 'range',
start: 0,
end: 24,
unit: 'items', // Range unit (default: 'items')
rangeHeader: 'Range' // Header name (default: 'Range')
}
});
// Sends: Range: items=0-24
```

## Retry with Exponential Backoff

Configure automatic retry for failed requests:

```typescript
const response = await postPingPostRequest({
payload: message,
server: 'https://api.example.com',
baseUrl: 'https://api.example.com',
retry: {
maxRetries: 3, // Maximum retry attempts (default: 3)
initialDelayMs: 1000, // Initial delay before first retry (default: 1000)
Expand All @@ -396,7 +317,7 @@ Customize request behavior with hooks:
```typescript
const response = await postPingPostRequest({
payload: message,
server: 'https://api.example.com',
baseUrl: 'https://api.example.com',
hooks: {
// Modify request before sending
beforeRequest: async (params) => {
Expand Down Expand Up @@ -459,7 +380,7 @@ const params = new UserItemsParameters({
});

const response = await getGetUserItem({
server: 'https://api.example.com',
baseUrl: 'https://api.example.com',
parameters: params // Replaces {userId} and {itemId} in path
});
```
Expand All @@ -477,7 +398,7 @@ const headers = new ItemRequestHeaders({
});

const response = await putUpdateUserItem({
server: 'https://api.example.com',
baseUrl: 'https://api.example.com',
parameters: params,
payload: itemData,
requestHeaders: headers // Type-safe headers
Expand All @@ -491,12 +412,12 @@ Add custom headers or query parameters to any request:
```typescript
const response = await postPingPostRequest({
payload: message,
server: 'https://api.example.com',
baseUrl: 'https://api.example.com',
additionalHeaders: {
'X-Custom-Header': 'value',
'Accept-Language': 'en-US'
},
queryParams: {
additionalQueryParams: {
include: 'metadata',
format: 'detailed'
}
Expand All @@ -519,7 +440,7 @@ operations:

```typescript
const response = await getItemRequest({
server: 'https://api.example.com',
baseUrl: 'https://api.example.com',
parameters: params
});

Expand Down
12 changes: 11 additions & 1 deletion src/codegen/generators/typescript/channels/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export async function generateTypeScriptChannelsForOpenAPI(
openapiDocument,
payloads,
parameters,
headers,
oauth2Enabled
);

Expand Down Expand Up @@ -136,6 +137,7 @@ function processOpenAPIOperations(
openapiDocument: OpenAPIDocument,
payloads: TypeScriptPayloadRenderType,
parameters: TypeScriptParameterRenderType,
headers: TypeScriptHeadersRenderType,
oauth2Enabled: boolean
): ReturnType<typeof renderHttpFetchClient>[] {
const renders: ReturnType<typeof renderHttpFetchClient>[] = [];
Expand All @@ -152,6 +154,7 @@ function processOpenAPIOperations(
path,
payloads,
parameters,
headers,
oauth2Enabled
);
if (render) {
Expand All @@ -172,6 +175,7 @@ function processOperation(
path: string,
payloads: TypeScriptPayloadRenderType,
parameters: TypeScriptParameterRenderType,
headers: TypeScriptHeadersRenderType,
oauth2Enabled: boolean
): ReturnType<typeof renderHttpFetchClient> | undefined {
// eslint-disable-next-line security/detect-object-injection
Expand Down Expand Up @@ -202,6 +206,10 @@ function processOperation(
// eslint-disable-next-line security/detect-object-injection
const parameterModel = parameters.channelModels[operationId];

// Look up headers
// eslint-disable-next-line security/detect-object-injection
const headersModel = headers.channelModels[operationId];

// Get message types - handle undefined payloads
const requestMessageInfo = requestPayload
? getMessageTypeAndModule(requestPayload)
Expand Down Expand Up @@ -257,11 +265,13 @@ function processOperation(
channelParameters: parameterModel?.model as
| ConstrainedObjectModel
| undefined,
channelHeaders: headersModel?.model as ConstrainedObjectModel | undefined,
includesStatusCodes: replyIncludesStatusCodes,
description,
deprecated,
oauth2Enabled,
hasSerializeUrl: parameterModel !== undefined
hasSerializeUrl: parameterModel !== undefined,
hasSerializeHeaders: headersModel !== undefined
});

// Grouping metadata for the `organization` option (consumed in
Expand Down
Loading
Loading