@bpmlib/utils-sarequest
Class-based HTTP client dengan automatic JWT token management dan Laravel route mapping support
Versi: 0.1.1
Kategori: Pure Utils
TL;DR
HTTP client wrapper untuk axios yang menyediakan automatic JWT authentication flow dengan token refresh, type-safe Laravel route mapping, dan chainable API untuk request configuration. Library ini handle token issuance, refresh, retry logic, dan menyimpan tokens di browser cookies secara otomatis.
Exports:
import { RequestInstance } from '@bpmlib/utils-sarequest';
import type {
HttpMethod,
ResponseType,
RouteDict,
RequestConfig,
TokenResponse
} from '@bpmlib/utils-sarequest';Installation & Setup
Requirements
Runtime:
- Browser environment (menggunakan
document.cookie) - TypeScript 5.0+
Peer Dependencies
Library ini memerlukan peer dependencies berikut:
Wajib:
npm install axios@^1.13.2| Dependency | Versi | Status | Deskripsi |
|---|---|---|---|
axios | ^1.13.2 | Required | HTTP client foundation |
PEER DEPENDENCY
Vue terdaftar sebagai peer dependency di package.json namun TIDAK digunakan dalam library. Ini adalah artifact dari copy-paste dan bisa diabaikan. Library ini adalah Pure Utils yang framework-agnostic.
Package Installation
npm install @bpmlib/utils-sarequestyarn add @bpmlib/utils-sarequestpnpm add @bpmlib/utils-sarequestbun install @bpmlib/utils-sarequestImport
Basic Import:
import RequestInstance from '@bpmlib/utils-sarequest';With Types:
import RequestInstance, {
type HttpMethod,
type ResponseType,
type RouteDict,
type RequestConfig,
type TokenResponse,
type RouteParams
} from '@bpmlib/utils-sarequest';Quick Start
Basic Usage
Contoh paling sederhana untuk setup dan request:
import RequestInstance from '@bpmlib/utils-sarequest';
// Define route dictionary
const routes = {
get: {
'users.index': '/users',
'users.show': '/users/{id}',
},
post: {
'users.store': '/users',
}
};
// Create instance
const api = new RequestInstance(
'https://api.example.com',
routes,
'myservice'
);
// Make request
const users = await api.get('users.index');
const user = await api.get('users.show', 123);Key Points:
- Route dictionary maps route names ke URL patterns
- Service name digunakan untuk cookie storage namespacing
- Authentication handled automatically
With Custom Configuration
Override default authentication endpoints dan token prefixes:
const api = new RequestInstance(
'https://api.example.com',
routes,
'myservice',
{
issueUrl: '/auth/token/create', // Default: /api/token/issue
refreshUrl: '/auth/token/refresh', // Default: /api/token/refresh
accessTokenPrefix: 'access_', // Default: tkaac_
refreshTokenPrefix: 'refresh_' // Default: tkarf_
}
);Use Case: Customize untuk match dengan backend authentication endpoints.
TypeScript Integration
Define route types untuk type-safe route names dan parameter extraction:
// Define route type structure
type MyRoutes = {
get: {
'users.index': '/users';
'users.show': '/users/{id}';
'posts.show': '/posts/{postId}/comments/{commentId}';
};
post: {
'users.store': '/users';
};
};
// Create typed instance
const api = new RequestInstance<MyRoutes>(
'https://api.example.com',
routes,
'myservice'
);
// Type-safe requests with autocomplete
const response = await api.get<User>('users.show', 123);
// ^^^^ response type
// ^^^^^^^^^^^ autocomplete route names
// Parameter extraction works automatically
api.get('posts.show', { postId: 1, commentId: 5 }); // Type-safe paramsKey Points:
- Route names autocomplete
- Parameters type-checked based on URL pattern
- Response types via generics
Core Concepts
Automatic JWT Authentication Flow
Library menggunakan axios interceptors untuk handle authentication secara automatic tanpa manual intervention.
Request Interceptor Flow:
- Check Access Token - Cek apakah access token exists di cookies
- Attach or Refresh:
- Jika access token ada → attach ke
Authorizationheader - Jika tidak ada tapi refresh token ada → call refresh endpoint → set tokens → attach
- Jika kedua tokens tidak ada → call issue endpoint → set initial tokens → attach
- Jika access token ada → attach ke
Response Interceptor Flow:
- On 401 Error - Jika response status 401 (Unauthorized):
- Mark request dengan
_retryflag - Call
refreshingToken()untuk get new access token - Update
Authorizationheader dengan token baru - Retry original request
- Mark request dengan
- On Success - Return response as-is
Automatic Behavior:
- Setiap request otomatis include valid token
- Token refresh terjadi transparently saat expired
- Failed refresh triggers re-authentication
- Retry logic prevents request failures dari expired tokens
Route Dictionary & URL Mapping
Route dictionary memetakan route names (Laravel-style) ke URL patterns dengan parameter placeholders.
How it works:
const routes = {
get: {
'users.show': '/users/{id}',
'posts.comments': '/posts/{postId}/comments/{commentId}',
'files.download': '/files/{id}/download/{filename?}' // {param?} = optional
}
};Parameter Extraction:
{param}- Required parameter{param?}- Optional parameter (bisa omitted)- Parameters extracted via generic types automatically
Usage Patterns:
- Array:
api.get('posts.comments', [1, 5]) - Object:
api.get('posts.comments', { postId: 1, commentId: 5 }) - Spread:
api.get('posts.comments', 1, 5)
Error Handling:
- Missing required parameter → throw Error dengan descriptive message
- Missing optional parameter → remove dari URL
- Route not found → log warning, return
/
Chainable Request Configuration
Semua configuration methods return this untuk method chaining.
Pattern:
api
.setHeader('X-Custom', 'value')
.setUrlParam('page', 1)
.setBody({ data: 'value' })
.post('endpoint');Auto-reset: Config di-reset setelah setiap request untuk prevent config leakage antar requests.
Available Chainable Methods:
setBody()- Set request body (JSON atau FormData)setHeader()/setHeaders()- Set request headerssetUrlParam()/setUrlParams()- Set URL query parameterssetResponseType()- Set expected response type
Token Storage Strategy
Tokens disimpan di browser cookies dengan automatic lifetime management.
Cookie Naming:
- Access Token:
{accessTokenPrefix}{service}→ Default:tkaac_myservice - Refresh Token:
{refreshTokenPrefix}{service}→ Default:tkarf_myservice
Cookie Configuration:
- Access token lifetime: 60 minutes (hardcoded)
- Refresh token lifetime: 24 hours (hardcoded)
- Flags:
SameSite=None; Secureuntuk cross-origin support - Path:
/(available untuk entire site)
Service Namespacing: Multiple service instances bisa coexist dengan service name berbeda. Setiap service punya isolated token storage.
const api1 = new RequestInstance(url, routes, 'service1'); // Cookies: tkaac_service1, tkarf_service1
const api2 = new RequestInstance(url, routes, 'service2'); // Cookies: tkaac_service2, tkarf_service2FormData Conversion
Method setBody() dengan asFormData: true automatically converts JavaScript objects ke FormData dengan nested object/array handling.
Conversion Rules:
| Input Type | FormData Behavior |
|---|---|
FileList | Append each file as key[index] |
File | Append directly |
Date | Convert to ISO string |
Array | Recursive dengan key[index] pattern |
Object | Recursive dengan dot notation (parent.child) |
| Primitive | Convert to string |
null | Skip (tidak di-append) |
Example:
const data = {
name: 'John',
profile: {
age: 25,
tags: ['admin', 'user']
},
avatar: fileInput.files[0]
};
api.setBody(data, true);
// FormData entries:
// name: "John"
// profile.age: "25"
// profile.tags[0]: "admin"
// profile.tags[1]: "user"
// avatar: [File object]Use Case: File uploads, multipart forms, atau backend yang expect FormData format.
API Reference
Classes
RequestInstance
Class-based HTTP client dengan automatic JWT authentication dan Laravel-style route mapping.
Generic Type Parameters:
RequestInstance<TRoutes extends Record<HttpMethod, Record<string, string>>>TRoutes- Route type definition untuk type-safe route names dan parameters
Constructor
constructor(
baseURL: string,
routes: RouteDict,
service: string,
options?: {
issueUrl?: string;
refreshUrl?: string;
accessTokenPrefix?: string;
refreshTokenPrefix?: string;
}
)Create new RequestInstance dengan configuration.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
baseURL | string | - | Base URL untuk semua requests |
routes | RouteDict | - | Route dictionary mapping Lihat selengkapnya |
service | string | - | Service identifier untuk token storage namespacing |
options | object | {} | Optional configuration Lihat selengkapnya |
routes
Route dictionary yang map route names ke URL patterns.
Structure:
{
get?: { [routeName: string]: string },
post?: { [routeName: string]: string },
put?: { [routeName: string]: string },
patch?: { [routeName: string]: string },
delete?: { [routeName: string]: string }
}Contoh:
const routes = {
get: {
'users.index': '/users',
'users.show': '/users/{id}',
},
post: {
'users.store': '/users',
'users.update': '/users/{id}',
},
delete: {
'users.destroy': '/users/{id}',
}
};URL Pattern Syntax:
{param}- Required parameter{param?}- Optional parameter
options
Optional configuration object untuk customize authentication dan token storage.
Properties:
issueUrl- Endpoint untuk initial token issuance (default:/api/token/issue)refreshUrl- Endpoint untuk token refresh (default:/api/token/refresh)accessTokenPrefix- Cookie prefix untuk access tokens (default:tkaac_)refreshTokenPrefix- Cookie prefix untuk refresh tokens (default:tkarf_)
Contoh:
const api = new RequestInstance(
'https://api.example.com',
routes,
'myservice',
{
issueUrl: '/auth/login',
refreshUrl: '/auth/refresh',
accessTokenPrefix: 'acc_',
refreshTokenPrefix: 'ref_'
}
);Use Case: Gunakan untuk override defaults ketika backend menggunakan custom authentication endpoints atau naming conventions berbeda.
Request Configuration Methods
setBody()
setBody(data: Record<string, unknown> | FormData, asFormData?: boolean): thisSet request body payload dengan optional FormData conversion.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
data | Record<string, unknown> | FormData | - | Request payload object atau FormData instance |
asFormData | boolean | false | Convert object to FormData Lihat selengkapnya |
Returns: this - Instance untuk chaining
asFormData
Flag untuk automatic conversion dari JavaScript object ke FormData dengan nested structure handling.
Behavior:
false(default) - Send as JSON denganContent-Type: application/jsontrue- Convert to FormData denganContent-Type: multipart/form-data
Conversion Rules: Lihat Core Concepts: FormData Conversion
Contoh:
// JSON body
api.setBody({ name: 'John', email: 'john@example.com' });
// FormData conversion
api.setBody({
name: 'John',
avatar: fileInput.files[0],
metadata: { role: 'admin' }
}, true);Use Case:
asFormData: false- REST API yang expect JSONasFormData: true- File uploads, multipart forms, legacy APIs
setHeader()
setHeader(key: string, value: string): thisSet single request header.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
key | string | - | Header name |
value | string | - | Header value |
Returns: this - Instance untuk chaining
Catatan: Content-Type hanya bisa di-set jika belum di-set oleh setBody().
setHeaders()
setHeaders(headers: Record<string, string>): thisSet multiple request headers sekaligus.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
headers | Record<string, string> | - | Object dengan key-value pairs untuk headers |
Returns: this - Instance untuk chaining
setUrlParam()
setUrlParam(key: string, value: string | number | boolean): thisSet single URL query parameter.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
key | string | - | Parameter name |
value | string | number | boolean | - | Parameter value |
Returns: this - Instance untuk chaining
setUrlParams()
setUrlParams(params: Record<string, string | number | boolean>): thisSet multiple URL query parameters sekaligus.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
params | Record<string, string | number | boolean> | - | Object dengan key-value pairs untuk URL parameters |
Returns: this - Instance untuk chaining
setResponseType()
setResponseType(type?: ResponseType): thisSet expected response type untuk axios request.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
type | ResponseType | 'json' | Expected response type |
Returns: this - Instance untuk chaining
Use Case: Gunakan untuk file downloads ('blob'), raw text ('text'), atau binary data ('arraybuffer').
HTTP Methods
get()
get<T = unknown, TRoute extends keyof TRoutes['get'] & string>(
routeName: TRoute,
...routeParam: RouteParams<TRoutes['get'][TRoute]>
): Promise<AxiosResponse<T>>Execute GET request ke route yang ditentukan.
Type Parameters
T- Response data type (default:unknown)TRoute- Route name type (auto-inferred dari route dictionary)
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
routeName | TRoute | - | Route name dari route dictionary |
routeParam | RouteParams<...> | - | Route parameters (array, object, atau spread) |
Returns: Promise<AxiosResponse<T>> - Axios response dengan typed data
Catatan: Request config di-reset setelah execution.
post()
post<T = unknown, TRoute extends keyof TRoutes['post'] & string>(
routeName: TRoute,
...routeParam: RouteParams<TRoutes['post'][TRoute]>
): Promise<AxiosResponse<T>>Execute POST request ke route yang ditentukan.
Type Parameters
T- Response data type (default:unknown)TRoute- Route name type (auto-inferred dari route dictionary)
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
routeName | TRoute | - | Route name dari route dictionary |
routeParam | RouteParams<...> | - | Route parameters (array, object, atau spread) |
Returns: Promise<AxiosResponse<T>> - Axios response dengan typed data
Catatan: Request body harus di-set dengan setBody() sebelumnya.
put()
put<T = unknown, TRoute extends keyof TRoutes['put'] & string>(
routeName: TRoute,
...routeParam: RouteParams<TRoutes['put'][TRoute]>
): Promise<AxiosResponse<T>>Execute PUT request ke route yang ditentukan.
Type Parameters
T- Response data type (default:unknown)TRoute- Route name type (auto-inferred dari route dictionary)
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
routeName | TRoute | - | Route name dari route dictionary |
routeParam | RouteParams<...> | - | Route parameters (array, object, atau spread) |
Returns: Promise<AxiosResponse<T>> - Axios response dengan typed data
patch()
patch<T = unknown, TRoute extends keyof TRoutes['patch'] & string>(
routeName: TRoute,
...routeParam: RouteParams<TRoutes['patch'][TRoute]>
): Promise<AxiosResponse<T>>Execute PATCH request ke route yang ditentukan.
Type Parameters
T- Response data type (default:unknown)TRoute- Route name type (auto-inferred dari route dictionary)
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
routeName | TRoute | - | Route name dari route dictionary |
routeParam | RouteParams<...> | - | Route parameters (array, object, atau spread) |
Returns: Promise<AxiosResponse<T>> - Axios response dengan typed data
delete()
delete<T = unknown, TRoute extends keyof TRoutes['delete'] & string>(
routeName: TRoute,
...routeParam: RouteParams<TRoutes['delete'][TRoute]>
): Promise<AxiosResponse<T>>Execute DELETE request ke route yang ditentukan.
Type Parameters
T- Response data type (default:unknown)TRoute- Route name type (auto-inferred dari route dictionary)
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
routeName | TRoute | - | Route name dari route dictionary |
routeParam | RouteParams<...> | - | Route parameters (array, object, atau spread) |
Returns: Promise<AxiosResponse<T>> - Axios response dengan typed data
Token Management Methods
issueToken()
async issueToken(): Promise<string>Request new token pair dari authentication server (initial authentication).
Returns: Promise<string> - Access token string
Behavior:
- POST ke
issueUrldengan{ service: this.service } - Expect response:
{ access: string, refresh: string } - Store both tokens di cookies
- Return access token
Error Handling:
- Network failure → Delete existing tokens → Throw error
- Invalid response → Delete existing tokens → Throw error
Catatan: Method ini di-call automatically oleh request interceptor jika tidak ada tokens.
refreshingToken()
async refreshingToken(): Promise<string>Refresh current access token menggunakan refresh token.
Returns: Promise<string> - New access token string
Behavior:
- Get refresh token dari cookies
- POST ke
refreshUrldengan{ service: this.service, refresh: refreshToken } - Expect response:
{ access: string, refresh: string } - Update both tokens di cookies
- Return new access token
Error Handling:
- No refresh token → Throw error "No refresh token available"
- Network/auth failure → Delete tokens → Re-throw error
Catatan: Method ini di-call automatically oleh request interceptor saat access token expired dan oleh response interceptor pada 401 errors.
getAccessTk()
getAccessTk(): string | nullGet current access token dari browser cookies.
Returns: string | null - Access token atau null jika tidak ada
Cookie Name: {accessTokenPrefix}{service}
getRefreshTk()
getRefreshTk(): string | nullGet current refresh token dari browser cookies.
Returns: string | null - Refresh token atau null jika tidak ada
Cookie Name: {refreshTokenPrefix}{service}
setAccessRefreshCookie()
setAccessRefreshCookie(access: string, refresh: string): voidManually set both access dan refresh tokens di cookies.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
access | string | - | Access token value |
refresh | string | - | Refresh token value |
Cookie Lifetimes:
- Access token: 60 minutes
- Refresh token: 24 hours
Use Case: Set tokens manually setelah custom authentication flow.
deleteAccessRefreshCookie()
deleteAccessRefreshCookie(): voidDelete both access dan refresh tokens dari browser cookies.
Use Case: Logout, authentication failure recovery.
Cookie Management Methods
setCookie()
setCookie(name: string, value: string, lifetimeMinute?: number): voidSet browser cookie dengan configurable lifetime.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
name | string | - | Cookie name |
value | string | - | Cookie value |
lifetimeMinute | number | undefined | Lifetime dalam menit (undefined = session cookie) |
Cookie Flags: SameSite=None; Secure; path=/
getCookie()
getCookie(name: string): string | nullGet browser cookie value by name.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
name | string | - | Cookie name |
Returns: string | null - Cookie value atau null jika tidak ditemukan
eraseCookie()
eraseCookie(name: string): voidDelete browser cookie by name.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
name | string | - | Cookie name yang akan dihapus |
Behavior: Set Max-Age=-99999999 untuk force expiration.
Utility Methods
useUrl()
useUrl<TRoute extends string>(
routeName: string,
method?: HttpMethod,
...params: RouteParams<TRoute>
): stringGenerate URL dari route name dengan parameter replacement.
Type Parameters
TRoute- Route URL pattern string type
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
routeName | string | - | Route name dari dictionary |
method | HttpMethod | 'get' | HTTP method untuk lookup route |
params | RouteParams<TRoute> | - | Route parameters (array, object, atau spread) |
Returns: string - Final URL dengan replaced parameters
Error Handling:
- Route not found → Log warning → Return
/ - Missing required parameter → Throw error dengan descriptive message
- Missing optional parameter → Remove dari URL
Contoh:
const url = api.useUrl('users.show', 'get', 123);
// Returns: '/users/123'
const url2 = api.useUrl('posts.comments', 'get', { postId: 1, commentId: 5 });
// Returns: '/posts/1/comments/5'getConfig()
getConfig(): AxiosRequestConfigGet current axios configuration object (params, headers, responseType).
Returns: AxiosRequestConfig - Current request configuration
Catatan: Body (data) tidak included dalam returned config.
Types
HttpMethod
type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete';HTTP method types yang supported untuk route dictionary dan requests.
ResponseType
type ResponseType = 'json' | 'blob' | 'arraybuffer' | 'document' | 'text' | 'stream';Axios response types yang supported untuk setResponseType().
Use Cases:
'json'- Default, API responses'blob'- File downloads'text'- Raw text content'arraybuffer'- Binary data'document'- HTML/XML documents'stream'- Streaming responses
RouteDict
type RouteDict = Partial<Record<HttpMethod, Record<string, string>>>;Route dictionary structure yang map HTTP methods ke route definitions.
Structure:
{
get?: { [routeName: string]: string },
post?: { [routeName: string]: string },
put?: { [routeName: string]: string },
patch?: { [routeName: string]: string },
delete?: { [routeName: string]: string }
}Contoh:
const routes: RouteDict = {
get: {
'users.index': '/users',
'users.show': '/users/{id}'
},
post: {
'users.store': '/users'
}
};RequestConfig
interface RequestConfig {
headers: Record<string, string>;
params: Record<string, string | number | boolean>;
data: Record<string, unknown> | FormData | null;
responseType: ResponseType | null;
}Internal request configuration structure untuk track chainable config state.
Properties:
headers- HTTP headersparams- URL query parametersdata- Request body (JSON object atau FormData)responseType- Expected response type
Catatan: Internal type, tidak digunakan directly oleh users.
TokenResponse
interface TokenResponse {
access: string;
refresh: string;
}Expected response format dari authentication endpoints (issueUrl dan refreshUrl).
Required Structure: Backend harus return object dengan properties ini untuk authentication flow berfungsi.
RouteParams
type RouteParams<T extends string> =
ExtractParams<T> extends never
? []
: [Record<ExtractParams<T>, string | number>] | (string | number)[];Type helper yang extract parameter names dari URL pattern string dan create appropriate parameter type.
How it works:
// Route: '/users/{id}'
// RouteParams: [Record<'id', string | number>] | (string | number)[]
// Route: '/posts/{postId}/comments/{commentId}'
// RouteParams: [Record<'postId' | 'commentId', string | number>] | (string | number)[]
// Route: '/users' (no params)
// RouteParams: []Usage Patterns:
// Object (named params)
api.get('posts.comments', { postId: 1, commentId: 5 });
// Array (positional)
api.get('posts.comments', [1, 5]);
// Spread
api.get('posts.comments', 1, 5);Examples
Contains:
- 1. Basic HTTP Requests
- 2. Request Configuration (Body, Headers, Parameters)
- 3. Route Parameter Patterns
- 4. Response Type Handling
- 5. File Upload with Nested Data
- 6. TypeScript Type-Safe Routes
- 7. Error Handling Pattern
1. Basic HTTP Requests
Demonstrasi semua HTTP methods (GET, POST, PUT, PATCH, DELETE) dengan berbagai parameter patterns.
import RequestInstance from '@bpmlib/utils-sarequest';
const routes = {
get: {
'users.index': '/users',
'users.show': '/users/{id}',
},
post: {
'users.store': '/users',
},
put: {
'users.update': '/users/{id}',
},
patch: {
'users.partial': '/users/{id}',
},
delete: {
'users.destroy': '/users/{id}',
}
};
const api = new RequestInstance(
'https://api.example.com',
routes,
'myservice'
);
// GET - Simple list
const users = await api.get('users.index');
// GET - With route parameter
const user = await api.get('users.show', 123);
// GET - With URL query parameters
const filtered = await api
.setUrlParam('page', 1)
.setUrlParam('limit', 10)
.get('users.index');
// POST - Create new resource
const newUser = await api
.setBody({ name: 'John', email: 'john@example.com' })
.post('users.store');
// PUT - Full update
const updated = await api
.setBody({ name: 'Jane Doe', email: 'jane@example.com' })
.put('users.update', 123);
// PATCH - Partial update
const patched = await api
.setBody({ name: 'Jane Smith' })
.patch('users.partial', 123);
// DELETE - Remove resource
await api.delete('users.destroy', 123);Key Takeaways:
- GET requests tidak perlu body
- POST/PUT/PATCH require
setBody()untuk send data - Route parameters passed setelah route name
- URL query parameters via
setUrlParam()/setUrlParams()
2. Request Configuration (Body, Headers, Parameters)
Demonstrasi chainable configuration methods untuk customize requests.
// JSON body
const response1 = await api
.setBody({ name: 'John', active: true })
.post('users.store');
// FormData body (automatic conversion)
const response2 = await api
.setBody({
name: 'John',
avatar: fileInput.files[0],
settings: { theme: 'dark', locale: 'id' }
}, true) // true = convert to FormData
.post('users.upload');
// Custom headers
const response3 = await api
.setHeader('X-Custom-Header', 'value')
.setHeader('X-API-Key', 'secret123')
.get('users.index');
// Multiple headers at once
const response4 = await api
.setHeaders({
'X-Request-ID': 'uuid-123',
'X-Client-Version': '1.0.0'
})
.get('users.index');
// URL query parameters
const response5 = await api
.setUrlParam('filter', 'active')
.setUrlParam('sort', 'name')
.get('users.index');
// Multiple URL params at once
const response6 = await api
.setUrlParams({
page: 1,
limit: 20,
sort: 'name',
order: 'asc',
active: true
})
.get('users.index');
// Complete chaining example
const response7 = await api
.setHeaders({
'X-Custom-Header': 'value',
'X-Request-ID': '123'
})
.setUrlParams({ page: 1, limit: 10 })
.setBody({ filter: { role: 'admin' } })
.setResponseType('json')
.post('users.search');Key Takeaways:
- All config methods chainable
setBody()dengantrueconverts to FormData- Headers dan params bisa di-set individual atau batch
- Config auto-reset setelah request executed
3. Route Parameter Patterns
Demonstrasi berbagai cara passing route parameters (array, object, spread, optional).
const routes = {
get: {
'posts.show': '/posts/{id}',
'posts.comments': '/posts/{postId}/comments/{commentId}',
'users.posts': '/users/{userId}/posts/{postId?}', // postId is optional
}
};
const api = new RequestInstance('https://api.example.com', routes, 'myservice');
// Single parameter - direct value
await api.get('posts.show', 123);
// → GET /posts/123
// Multiple parameters - array (positional)
await api.get('posts.comments', [1, 5]);
// → GET /posts/1/comments/5
// Multiple parameters - object (named)
await api.get('posts.comments', { postId: 1, commentId: 5 });
// → GET /posts/1/comments/5
// Multiple parameters - spread arguments
await api.get('posts.comments', 1, 5);
// → GET /posts/1/comments/5
// Optional parameters - omitted
await api.get('users.posts', { userId: 1 });
// → GET /users/1/posts/
// Optional parameters - included
await api.get('users.posts', { userId: 1, postId: 5 });
// → GET /users/1/posts/5
// Optional parameters - array format
await api.get('users.posts', [1]);
// → GET /users/1/posts/
await api.get('users.posts', [1, 5]);
// → GET /users/1/posts/5Key Takeaways:
- Single param: pass directly
- Multiple params: array, object, atau spread
- Object pattern best untuk readability
- Optional params: omit atau include as needed
- Missing required param → throws error
4. Response Type Handling
Demonstrasi berbagai response types untuk different use cases (file downloads, raw text, etc).
const routes = {
get: {
'files.download': '/files/{id}/download',
'content.raw': '/content/{id}/raw',
'images.get': '/images/{id}',
'reports.export': '/reports/{id}/export',
}
};
const api = new RequestInstance('https://api.example.com', routes, 'myservice');
// Download file as blob
const fileResponse = await api
.setResponseType('blob')
.get('files.download', 123);
// Create download link
const blob = fileResponse.data;
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'file.pdf';
link.click();
window.URL.revokeObjectURL(url);
// Get raw text content
const textResponse = await api
.setResponseType('text')
.get('content.raw', 456);
console.log(textResponse.data); // Raw text string
// Get binary data (arraybuffer)
const binaryResponse = await api
.setResponseType('arraybuffer')
.get('images.get', 789);
const buffer = binaryResponse.data;
// Stream response
const streamResponse = await api
.setResponseType('stream')
.get('reports.export', 999);
// Default JSON (no setResponseType needed)
const jsonResponse = await api.get('files.download', 123);
const data = jsonResponse.data; // Parsed JSON objectKey Takeaways:
'blob'- File downloads'text'- Raw text content'arraybuffer'- Binary data'stream'- Streaming responses'json'- Default, no need to set explicitly
5. File Upload with Nested Data
Demonstrasi FormData conversion dengan file uploads dan nested objects/arrays.
const api = new RequestInstance('https://api.example.com', routes, 'myservice');
// Complex form data with files and nested structures
const formInput = {
// Simple fields
name: 'Product Name',
price: 99.99,
active: true,
// Date field (auto-converted to ISO string)
publishedAt: new Date(),
// File upload
thumbnail: fileInput.files[0],
// Multiple files
gallery: galleryInput.files, // FileList
// Nested object
metadata: {
category: 'Electronics',
brand: 'BrandName',
specs: {
weight: 1.5,
dimensions: '10x20x5'
}
},
// Array of primitives
tags: ['featured', 'new', 'sale'],
// Array of objects
variants: [
{ color: 'red', stock: 10 },
{ color: 'blue', stock: 5 }
]
};
const response = await api
.setBody(formInput, true) // true = convert to FormData
.post('products.store');
// Resulting FormData structure:
// name: "Product Name"
// price: "99.99"
// active: "true"
// publishedAt: "2024-12-25T10:30:00.000Z"
// thumbnail: [File object]
// gallery[0]: [File object]
// gallery[1]: [File object]
// metadata.category: "Electronics"
// metadata.brand: "BrandName"
// metadata.specs.weight: "1.5"
// metadata.specs.dimensions: "10x20x5"
// tags[0]: "featured"
// tags[1]: "new"
// tags[2]: "sale"
// variants[0].color: "red"
// variants[0].stock: "10"
// variants[1].color: "blue"
// variants[1].stock: "5"Key Takeaways:
setBody(data, true)triggers automatic conversion- Files handled directly (File, FileList)
- Dates converted to ISO strings
- Objects use dot notation (
parent.child) - Arrays use index notation (
array[0]) - Nested structures preserved
nullvalues skipped
6. TypeScript Type-Safe Routes
Demonstrasi TypeScript generics untuk type-safe route names dan parameter extraction.
// Define route structure as type
type MyRoutes = {
get: {
'users.index': '/users';
'users.show': '/users/{id}';
'posts.show': '/posts/{postId}/comments/{commentId}';
};
post: {
'users.store': '/users';
'users.update': '/users/{id}';
};
delete: {
'users.destroy': '/users/{id}';
};
};
// Define response types
interface User {
id: number;
name: string;
email: string;
}
interface CreateUserPayload {
name: string;
email: string;
password: string;
}
// Create typed instance
const api = new RequestInstance<MyRoutes>(
'https://api.example.com',
routes,
'myservice'
);
// Type-safe route names (autocomplete works)
const users = await api.get<User[]>('users.index');
// ^^^^^ Response type
// ^^^^^^^^^^^ Autocomplete route names
// Type-safe parameters (compiler checks param types)
const user = await api.get<User>('users.show', 123);
// ^^^ Type: number | string
// Multiple params - object keys type-checked
const post = await api.get('posts.show', {
postId: 1, // ✓ Valid
commentId: 5 // ✓ Valid
// extraParam: 10 // ✗ Error: Object literal may only specify known properties
});
// POST with typed response
const newUser = await api
.setBody<CreateUserPayload>({
name: 'John',
email: 'john@example.com',
password: 'secret'
})
.post<User>('users.store');
// Type inference works
newUser.data.id; // Type: number
newUser.data.name; // Type: string
newUser.data.email; // Type: string
// Wrong route name → Compile error
// const x = await api.get('users.invalid'); // ✗ Error
// Wrong HTTP method for route → Compile error
// const x = await api.post('users.index'); // ✗ Error (index only in GET)Key Takeaways:
- Generic type parameter untuk route structure
- Route names autocomplete di IDE
- Parameter types inferred dari URL patterns
- Response types via generic
<T> - Compile-time safety untuk route names dan params
- Full IntelliSense support
7. Error Handling Pattern
Demonstrasi proper error handling untuk network failures dan authentication errors.
import axios from 'axios';
import RequestInstance from '@bpmlib/utils-sarequest';
const api = new RequestInstance('https://api.example.com', routes, 'myservice');
// Basic try-catch
try {
const response = await api.get('users.show', 123);
console.log('User:', response.data);
} catch (error) {
console.error('Request failed:', error);
}
// Detailed error handling
try {
const response = await api
.setBody({ name: 'John' })
.post('users.store');
console.log('Created user:', response.data);
} catch (error) {
if (axios.isAxiosError(error)) {
// Axios-specific error
if (error.response) {
// Server responded with error status
console.error('Status:', error.response.status);
console.error('Data:', error.response.data);
console.error('Headers:', error.response.headers);
// Handle specific status codes
switch (error.response.status) {
case 400:
console.error('Bad Request - Validation failed');
break;
case 401:
console.error('Unauthorized - Token refresh failed');
// Note: 401 auto-handled by interceptor, this only fires if refresh fails
break;
case 403:
console.error('Forbidden - Insufficient permissions');
break;
case 404:
console.error('Not Found - Resource does not exist');
break;
case 500:
console.error('Server Error');
break;
default:
console.error('Unknown error status');
}
} else if (error.request) {
// Request sent but no response received
console.error('No response received:', error.request);
console.error('Network error or timeout');
} else {
// Error setting up request
console.error('Request setup error:', error.message);
}
} else {
// Non-axios error
console.error('Unexpected error:', error);
}
}
// Handle missing route parameters
try {
// Missing required parameter
await api.get('users.show'); // Throws error
} catch (error) {
console.error('Parameter error:', error.message);
// Error: "Parameter URL wajib bernama "id" belum terpetakan..."
}
// Authentication error handling
try {
const response = await api.get('protected.resource');
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 401) {
// Authentication failed even after retry
console.error('Authentication failed');
// Manual re-authentication if needed
await api.issueToken();
// Retry request
const retryResponse = await api.get('protected.resource');
}
}
// Async/await with proper cleanup
async function fetchUser(id: number) {
try {
const response = await api.get<User>('users.show', id);
return response.data;
} catch (error) {
// Log error
console.error(`Failed to fetch user ${id}:`, error);
// Return fallback or rethrow
throw error;
}
}Key Takeaways:
- Use
axios.isAxiosError()untuk type-safe error checking error.response- Server responded with error statuserror.request- No response received (network error)error.message- Request setup error- 401 errors auto-handled by interceptor
- Missing route params throw descriptive errors
- Always handle both network dan application errors