Stacks HTTP
Key Paths
- Core package:
storage/framework/core/http/src/ - Composables (useFetch/createFetch):
storage/framework/core/composables/src/useFetch.ts,storage/framework/core/composables/src/createFetch.ts - Buddy CLI command:
storage/framework/core/buddy/src/commands/http.ts - httx dependency (installed):
node_modules/@stacksjs/httx/ - Package:
@stacksjs/http(status codes),@stacksjs/httx(HTTP client),@stacksjs/composables(useFetch/createFetch)
Source Files
http/src/
└── index.ts # Response enum — all HTTP status codes (100-511)
composables/src/
├── useFetch.ts # Chainable reactive fetch composable (builder pattern)
└── createFetch.ts # Factory for pre-configured useFetch with baseUrl
buddy/src/commands/
└── http.ts # `buddy http [domain]` CLI command using HttxClient
Response Enum — HTTP Status Codes (index.ts)
The @stacksjs/http package exports a single Response enum with all standard HTTP status codes:
import { Response } from '@stacksjs/http'
Response.HTTP_OK // 200
Response.HTTP_CREATED // 201
Response.HTTP_NO_CONTENT // 204
Response.HTTP_MOVED_PERMANENTLY // 301
Response.HTTP_NOT_MODIFIED // 304
Response.HTTP_BAD_REQUEST // 400
Response.HTTP_UNAUTHORIZED // 401
Response.HTTP_FORBIDDEN // 403
Response.HTTP_NOT_FOUND // 404
Response.HTTP_UNPROCESSABLE_ENTITY // 422
Response.HTTP_TOO_MANY_REQUESTS // 429
Response.HTTP_INTERNAL_SERVER_ERROR // 500
Response.HTTP_SERVICE_UNAVAILABLE // 503
Full Status Code Categories
- 1xx Informational:
HTTP_CONTINUE(100),HTTP_SWITCHING_PROTOCOLS(101) - 2xx Success:
HTTP_OK(200),HTTP_CREATED(201),HTTP_ACCEPTED(202),HTTP_NON_AUTHORITATIVE_INFORMATION(203),HTTP_NO_CONTENT(204),HTTP_RESET_CONTENT(205),HTTP_PARTIAL_CONTENT(206) - 3xx Redirection:
HTTP_MULTIPLE_CHOICES(300),HTTP_MOVED_PERMANENTLY(301),HTTP_FOUND(302),HTTP_SEE_OTHER(303),HTTP_NOT_MODIFIED(304),HTTP_USE_PROXY(305),HTTP_UNUSED(306),HTTP_TEMPORARY_REDIRECT(307),HTTP_PERMANENT_REDIRECT(308) - 4xx Client Error:
HTTP_BAD_REQUEST(400) throughHTTP_UNAVAILABLE_FOR_LEGAL_REASONS(451) — includesHTTP_I_AM_A_TEAPOT(418) - 5xx Server Error:
HTTP_INTERNAL_SERVER_ERROR(500) throughHTTP_NETWORK_AUTHENTICATION_REQUIRED(511)
HttxClient — Outbound HTTP Client (@stacksjs/httx)
The @stacksjs/httx package provides the HttxClient class for making outbound HTTP requests. It wraps Bun's fetch() with retry logic, timeouts, timing data, and Result-based error handling via ts-error-handling.
import { HttxClient } from '@stacksjs/httx'
const client = new HttxClient(config?: Partial<HttxConfig>)
const result = await client.request<T>(url: string, options: RequestOptions): Promise<Result<HttxResponse<T>, Error>>
HttxConfig
interface HttxConfig {
verbose?: boolean | string[] // Enable debug logging (or filter by category)
defaultHeaders?: Record<string, string>
baseUrl?: string // Prepended to all request URLs
timeout?: number // Request timeout in ms
retry?: RetryOptions // Default retry configuration
}
RequestOptions
interface RequestOptions {
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS'
query?: Record<string, string> // URL query parameters
form?: boolean // Send body as form-encoded
multipart?: boolean // Send body as multipart/form-data
json?: boolean // Send body as JSON
unix?: string // Unix socket path
proxy?: string // Proxy URL
downloadProgress?: (progress: number) => void
retry?: RetryOptions // Per-request retry override
stream?: boolean // Stream response
acceptHeader?: string // Override Accept header
verbose?: boolean
timeout?: number
body?: BodyInit | Record<string, string>
headers?: HeadersInit
}
RetryOptions
interface RetryOptions {
retries?: number // Max retry attempts
retryDelay?: number // Base delay between retries (ms)
retryOn?: number[] // HTTP status codes that trigger retry
shouldRetry?: (error: Error, attempt: number) => boolean // Custom retry predicate
}
HttxResponse
interface HttxResponse<T = unknown> {
status: number
statusText: string
headers: Headers
data: T
timings: {
start: number
end: number
duration: number // Total request duration in ms
}
}
Result Pattern (from ts-error-handling)
client.request() returns a Result<HttxResponse<T>, Error>. Use .match() to handle success/failure:
const result = await client.request('https://api.example.com/users', {
method: 'GET',
})
result.match({
ok: (response) => {
console.log(response.status) // 200
console.log(response.timings.duration) // e.g. 142 (ms)
console.log(response.data) // parsed response body
},
err: (error) => {
console.error(error.message)
},
})
Or use result.isOk / result.value / result.error directly.
Error Classes
class HttxError extends Error {
readonly context?: Record<string, unknown>
}
class HttxRequestError extends HttxError {
readonly method: string
readonly url: string
readonly statusCode?: number
}
class HttxTimeoutError extends HttxError {
readonly method: string
readonly url: string
readonly timeout: number
}
class HttxNetworkError extends HttxError {
readonly method: string
readonly url: string
readonly originalError?: Error
}
class HttxResponseError extends HttxRequestError {
readonly statusText: string
readonly responseBody?: unknown
}
Utility Exports
import { debugLog, sleep } from '@stacksjs/httx'
debugLog(category: string, message: string | (() => string), verbose?: boolean | string[]): void
sleep(ms: number): Promise<void>
Config Exports
import { config, defaultConfig, getConfig } from '@stacksjs/httx'
const config: HttxConfig // Synchronous access with default fallback
const defaultConfig: HttxConfig // Default configuration object
function getConfig(): Promise<HttxConfig> // Async config loader
useFetch — Reactive Fetch Composable (@stacksjs/composables)
A chainable, reactive fetch composable using the builder pattern. Uses native fetch() under the hood (not HttxClient). Returns reactive Ref values from @stacksjs/stx.
import { useFetch } from '@stacksjs/composables'
// Builder pattern: method -> format -> result
const { data, error, isFetching } = await useFetch('/api/posts').get().json()
FetchBuilder Interface
interface FetchBuilder {
get: () => FetchBuilder
post: (body?: string) => FetchBuilder
patch: (body?: string) => FetchBuilder
put: (body?: string) => FetchBuilder
delete: () => FetchBuilder
json: () => UseFetchResult
}
UseFetchResult Interface
interface UseFetchResult {
data: Ref<any> // Reactive ref — populated with parsed JSON on success
error: Ref<any> // Reactive ref — populated with error JSON or Error object
isFetching: Ref<boolean> // Starts as true, becomes false when request completes
then: (resolve: (value: { data: Ref<any>, error: Ref<any> }) => void) => Promise<void>
}
Usage Examples
import { useFetch } from '@stacksjs/composables'
// GET request
const { data, error } = await useFetch('/api/users').get().json()
// POST with JSON body
const { data, error } = await useFetch('/api/users')
.post(JSON.stringify({ name: 'Alice', email: 'alice@example.com' }))
.json()
// PATCH with body
const { data, error } = await useFetch('/api/users/1')
.patch(JSON.stringify({ name: 'Updated' }))
.json()
// DELETE
const { data, error } = await useFetch('/api/users/1').delete().json()
// Synchronous access (reactive — updates when request completes)
const result = useFetch('/api/items').get().json()
result.isFetching.value // true (synchronously, before microtask resolves)
// ... later ...
result.data.value // parsed JSON response
result.isFetching.value // false
How json() Works Internally
- Builds a
RequestInitwith the configured method andAccept: application/jsonheader - For POST/PATCH/PUT with a body, also sets
Content-Type: application/json - Calls native
fetch(url, init) - On success (
response.ok), parses JSON intodata.value - On HTTP error, parses JSON into
error.value - On network error, sets
error.valueto the caught Error - Always sets
isFetching.value = falseinfinally
createFetch — Pre-configured Fetch Factory (@stacksjs/composables)
Creates a reusable useFetch instance with a pre-configured base URL.
import { createFetch } from '@stacksjs/composables'
interface CreateFetchOptions {
baseUrl?: string // Base URL prepended to all requests
options?: RequestInit // Default options (currently unused in implementation)
}
const useApi = createFetch({ baseUrl: 'https://api.example.com' })
// URL becomes: https://api.example.com/users
const { data, error } = await useApi('/users').get().json()
URL Resolution
createFetch joins the base URL and path by stripping trailing/leading slashes:
baseUrl: 'https://api.example.com/'+'/users'→'https://api.example.com/users'baseUrl: 'https://api.example.com'+'users'→'https://api.example.com/users'
CLI Command — buddy http
buddy http [domain] # Send a GET request (defaults to config.app.url)
buddy http example.com # GET https://example.com
buddy http -v example.com # Verbose output
The CLI command uses HttxClient from @stacksjs/httx:
- Prepends
https://if the URL does not start withhttp - Logs status code, status text, and duration
- Prints response body (string or pretty-printed JSON)
Gotchas
@stacksjs/httpis status codes only — it exports a singleResponseenum with HTTP status code constants. It does NOT contain an HTTP client, request helpers, or fetch utilities- The actual HTTP client is
@stacksjs/httx— a separate package (not part of@stacksjs/http). It is used bybuddy httpand available for general use useFetchuses nativefetch(), not HttxClient — despite@stacksjs/httxbeing available, theuseFetchcomposable calls the globalfetch()directlyuseFetchonly supports.json()format — there is no.text(),.blob(), or.arrayBuffer()method. The only terminal method is.json()- Method chaining overrides previous method — calling
.post('body').get()will send a GET request (last method wins, body is cleared for GET/DELETE) createFetchignores theoptionsparameter — theCreateFetchOptions.optionsfield is declared in the interface but not used in the implementationuseFetchbody is only sent for POST/PATCH/PUT — GET and DELETE requests strip the body even if one was set- The
Responseenum name conflicts with the globalResponse— importingResponsefrom@stacksjs/httpshadows the native Fetch APIResponsetype. Use a named import alias if both are needed:import { Response as HttpStatus } from '@stacksjs/http' - HttxClient uses Result pattern —
request()returnsResult<HttxResponse, Error>fromts-error-handling, not a plain Promise. Use.match(),.isOk, or.value/.errorto unwrap useFetchreturns reactive Refs —data,error, andisFetchingareRef<T>from@stacksjs/stx. Access values via.valueproperty- No request interceptors or middleware — neither
useFetchnorHttxClientsupport request/response interceptors - For API route handling, use
@stacksjs/router—@stacksjs/httpand@stacksjs/httxare for outbound requests only, not for defining API endpoints