Skip to content

@bpmlib/utils-sarequest

Class-based HTTP client dengan automatic JWT token management dan Laravel route mapping support

Versi: 0.1.1
Kategori: Pure Utils

npm versionTypeScript


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:

ts
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:

bash
npm install axios@^1.13.2
DependencyVersiStatusDeskripsi
axios^1.13.2RequiredHTTP 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

bash
npm install @bpmlib/utils-sarequest
bash
yarn add @bpmlib/utils-sarequest
bash
pnpm add @bpmlib/utils-sarequest
bash
bun install @bpmlib/utils-sarequest

Import

Basic Import:

ts
import RequestInstance from '@bpmlib/utils-sarequest';

With Types:

ts
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:

ts
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:

ts
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:

ts
// 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 params

Key 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:

  1. Check Access Token - Cek apakah access token exists di cookies
  2. Attach or Refresh:
    • Jika access token ada → attach ke Authorization header
    • 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

Response Interceptor Flow:

  1. On 401 Error - Jika response status 401 (Unauthorized):
    • Mark request dengan _retry flag
    • Call refreshingToken() untuk get new access token
    • Update Authorization header dengan token baru
    • Retry original request
  2. 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:

ts
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:

ts
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 headers
  • setUrlParam() / setUrlParams() - Set URL query parameters
  • setResponseType() - 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; Secure untuk 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.

ts
const api1 = new RequestInstance(url, routes, 'service1');  // Cookies: tkaac_service1, tkarf_service1
const api2 = new RequestInstance(url, routes, 'service2');  // Cookies: tkaac_service2, tkarf_service2

FormData Conversion

Method setBody() dengan asFormData: true automatically converts JavaScript objects ke FormData dengan nested object/array handling.

Conversion Rules:

Input TypeFormData Behavior
FileListAppend each file as key[index]
FileAppend directly
DateConvert to ISO string
ArrayRecursive dengan key[index] pattern
ObjectRecursive dengan dot notation (parent.child)
PrimitiveConvert to string
nullSkip (tidak di-append)

Example:

ts
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:

ts
RequestInstance<TRoutes extends Record<HttpMethod, Record<string, string>>>
  • TRoutes - Route type definition untuk type-safe route names dan parameters
Constructor
ts
constructor(
  baseURL: string,
  routes: RouteDict,
  service: string,
  options?: {
    issueUrl?: string;
    refreshUrl?: string;
    accessTokenPrefix?: string;
    refreshTokenPrefix?: string;
  }
)

Create new RequestInstance dengan configuration.

Parameters

NameTypeDefaultDescription
baseURLstring-Base URL untuk semua requests
routesRouteDict-Route dictionary mapping Lihat selengkapnya
servicestring-Service identifier untuk token storage namespacing
optionsobject{}Optional configuration Lihat selengkapnya
routes

Route dictionary yang map route names ke URL patterns.

Structure:

ts
{
  get?: { [routeName: string]: string },
  post?: { [routeName: string]: string },
  put?: { [routeName: string]: string },
  patch?: { [routeName: string]: string },
  delete?: { [routeName: string]: string }
}

Contoh:

ts
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:

ts
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()
ts
setBody(data: Record<string, unknown> | FormData, asFormData?: boolean): this

Set request body payload dengan optional FormData conversion.

Parameters

NameTypeDefaultDescription
dataRecord<string, unknown> | FormData-Request payload object atau FormData instance
asFormDatabooleanfalseConvert 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 dengan Content-Type: application/json
  • true - Convert to FormData dengan Content-Type: multipart/form-data

Conversion Rules: Lihat Core Concepts: FormData Conversion

Contoh:

ts
// 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 JSON
  • asFormData: true - File uploads, multipart forms, legacy APIs
setHeader()
ts
setHeader(key: string, value: string): this

Set single request header.

Parameters

NameTypeDefaultDescription
keystring-Header name
valuestring-Header value

Returns: this - Instance untuk chaining

Catatan: Content-Type hanya bisa di-set jika belum di-set oleh setBody().

setHeaders()
ts
setHeaders(headers: Record<string, string>): this

Set multiple request headers sekaligus.

Parameters

NameTypeDefaultDescription
headersRecord<string, string>-Object dengan key-value pairs untuk headers

Returns: this - Instance untuk chaining

setUrlParam()
ts
setUrlParam(key: string, value: string | number | boolean): this

Set single URL query parameter.

Parameters

NameTypeDefaultDescription
keystring-Parameter name
valuestring | number | boolean-Parameter value

Returns: this - Instance untuk chaining

setUrlParams()
ts
setUrlParams(params: Record<string, string | number | boolean>): this

Set multiple URL query parameters sekaligus.

Parameters

NameTypeDefaultDescription
paramsRecord<string, string | number | boolean>-Object dengan key-value pairs untuk URL parameters

Returns: this - Instance untuk chaining

setResponseType()
ts
setResponseType(type?: ResponseType): this

Set expected response type untuk axios request.

Parameters

NameTypeDefaultDescription
typeResponseType'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()
ts
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

NameTypeDefaultDescription
routeNameTRoute-Route name dari route dictionary
routeParamRouteParams<...>-Route parameters (array, object, atau spread)

Returns: Promise<AxiosResponse<T>> - Axios response dengan typed data

Catatan: Request config di-reset setelah execution.

post()
ts
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

NameTypeDefaultDescription
routeNameTRoute-Route name dari route dictionary
routeParamRouteParams<...>-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()
ts
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

NameTypeDefaultDescription
routeNameTRoute-Route name dari route dictionary
routeParamRouteParams<...>-Route parameters (array, object, atau spread)

Returns: Promise<AxiosResponse<T>> - Axios response dengan typed data

patch()
ts
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

NameTypeDefaultDescription
routeNameTRoute-Route name dari route dictionary
routeParamRouteParams<...>-Route parameters (array, object, atau spread)

Returns: Promise<AxiosResponse<T>> - Axios response dengan typed data

delete()
ts
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

NameTypeDefaultDescription
routeNameTRoute-Route name dari route dictionary
routeParamRouteParams<...>-Route parameters (array, object, atau spread)

Returns: Promise<AxiosResponse<T>> - Axios response dengan typed data

Token Management Methods
issueToken()
ts
async issueToken(): Promise<string>

Request new token pair dari authentication server (initial authentication).

Returns: Promise<string> - Access token string

Behavior:

  1. POST ke issueUrl dengan { service: this.service }
  2. Expect response: { access: string, refresh: string }
  3. Store both tokens di cookies
  4. 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()
ts
async refreshingToken(): Promise<string>

Refresh current access token menggunakan refresh token.

Returns: Promise<string> - New access token string

Behavior:

  1. Get refresh token dari cookies
  2. POST ke refreshUrl dengan { service: this.service, refresh: refreshToken }
  3. Expect response: { access: string, refresh: string }
  4. Update both tokens di cookies
  5. 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()
ts
getAccessTk(): string | null

Get current access token dari browser cookies.

Returns: string | null - Access token atau null jika tidak ada

Cookie Name: {accessTokenPrefix}{service}

getRefreshTk()
ts
getRefreshTk(): string | null

Get current refresh token dari browser cookies.

Returns: string | null - Refresh token atau null jika tidak ada

Cookie Name: {refreshTokenPrefix}{service}

setAccessRefreshCookie()
ts
setAccessRefreshCookie(access: string, refresh: string): void

Manually set both access dan refresh tokens di cookies.

Parameters

NameTypeDefaultDescription
accessstring-Access token value
refreshstring-Refresh token value

Cookie Lifetimes:

  • Access token: 60 minutes
  • Refresh token: 24 hours

Use Case: Set tokens manually setelah custom authentication flow.

deleteAccessRefreshCookie()
ts
deleteAccessRefreshCookie(): void

Delete both access dan refresh tokens dari browser cookies.

Use Case: Logout, authentication failure recovery.

setCookie()
ts
setCookie(name: string, value: string, lifetimeMinute?: number): void

Set browser cookie dengan configurable lifetime.

Parameters

NameTypeDefaultDescription
namestring-Cookie name
valuestring-Cookie value
lifetimeMinutenumberundefinedLifetime dalam menit (undefined = session cookie)

Cookie Flags: SameSite=None; Secure; path=/

getCookie()
ts
getCookie(name: string): string | null

Get browser cookie value by name.

Parameters

NameTypeDefaultDescription
namestring-Cookie name

Returns: string | null - Cookie value atau null jika tidak ditemukan

eraseCookie()
ts
eraseCookie(name: string): void

Delete browser cookie by name.

Parameters

NameTypeDefaultDescription
namestring-Cookie name yang akan dihapus

Behavior: Set Max-Age=-99999999 untuk force expiration.

Utility Methods
useUrl()
ts
useUrl<TRoute extends string>(
  routeName: string,
  method?: HttpMethod,
  ...params: RouteParams<TRoute>
): string

Generate URL dari route name dengan parameter replacement.

Type Parameters

  • TRoute - Route URL pattern string type

Parameters

NameTypeDefaultDescription
routeNamestring-Route name dari dictionary
methodHttpMethod'get'HTTP method untuk lookup route
paramsRouteParams<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:

ts
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()
ts
getConfig(): AxiosRequestConfig

Get current axios configuration object (params, headers, responseType).

Returns: AxiosRequestConfig - Current request configuration

Catatan: Body (data) tidak included dalam returned config.

Types

HttpMethod

ts
type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete';

HTTP method types yang supported untuk route dictionary dan requests.

ResponseType

ts
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

ts
type RouteDict = Partial<Record<HttpMethod, Record<string, string>>>;

Route dictionary structure yang map HTTP methods ke route definitions.

Structure:

ts
{
  get?: { [routeName: string]: string },
  post?: { [routeName: string]: string },
  put?: { [routeName: string]: string },
  patch?: { [routeName: string]: string },
  delete?: { [routeName: string]: string }
}

Contoh:

ts
const routes: RouteDict = {
  get: {
    'users.index': '/users',
    'users.show': '/users/{id}'
  },
  post: {
    'users.store': '/users'
  }
};

RequestConfig

ts
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 headers
  • params - URL query parameters
  • data - Request body (JSON object atau FormData)
  • responseType - Expected response type

Catatan: Internal type, tidak digunakan directly oleh users.

TokenResponse

ts
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

ts
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:

ts
// 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:

ts
// 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

Demonstrasi semua HTTP methods (GET, POST, PUT, PATCH, DELETE) dengan berbagai parameter patterns.

ts
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.

ts
// 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() dengan true converts 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).

ts
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/5

Key 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).

ts
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 object

Key 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.

ts
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
  • null values skipped

6. TypeScript Type-Safe Routes

Demonstrasi TypeScript generics untuk type-safe route names dan parameter extraction.

ts
// 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.

ts
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 status
  • error.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