Every web application makes HTTP requests. A SaaS dashboard fetches analytics from an API. An e-commerce site charges cards through Stripe. A social app pulls user profiles from a backend. HTTP is the connective tissue of the modern web, and yet most programming languages treat it as an afterthought -- something you handle with a third-party library.
In JavaScript, the built-in fetch API arrived years after Node.js launched, and it still lacks features like automatic retries and request timeouts (the AbortController workaround is nobody's idea of elegant). In Python, the standard library's urllib is so painful that requests became the most downloaded package on PyPI. In Go, the built-in net/http client is decent but verbose -- a simple POST with JSON body takes 15 lines.
FLIN includes a complete HTTP client as a built-in. Sessions 063 and 064 designed and implemented it. Five functions. Zero imports. Automatic JSON handling. Configurable timeouts and retries. Everything a web developer needs to integrate with any API.
The Five Functions
http_get(url)
http_get(url, options)http_post(url, options)
http_put(url, options)
http_patch(url, options)
http_delete(url) http_delete(url, options) ```
Five functions for five HTTP methods. That is the entire API surface. Each function takes a URL and an optional options map, makes the request, and returns a response object. No constructing request objects. No configuring middleware chains. No importing adapters.
GET Requests: The Simple Case
The most common HTTP operation is fetching data. In FLIN, it is one line:
response = http_get("https://api.example.com/users")The response object contains everything you need:
response.status // 200
response.ok // true (status 200-299)
response.headers // Map of response headers
response.body // Raw response body (text)
response.json // Parsed JSON (if Content-Type is application/json)response.ok is a boolean that is true when the status code is in the 200-299 range. This eliminates the most common bug in HTTP client code: forgetting to check the status code. In JavaScript, fetch resolves successfully even for 404 and 500 responses -- a design choice that has caused millions of bugs. In FLIN, response.ok makes the check explicit and obvious.
response.json lazily parses the response body as JSON. If the body is not valid JSON, it returns none instead of throwing an error. This means you can safely access it without a try-catch block:
response = http_get("https://api.example.com/users"){if response.ok}
users = response.json
{if users != none}
{for user in users}
No try-catch. No error callbacks. No promise chains. Just conditional checks on values that are always present.
POST Requests: Sending Data
POST requests are where FLIN's automatic JSON handling shines:
response = http_post("https://api.example.com/users", {
body: {
name: "Juste Gnimavo",
email: "[email protected]",
role: "admin"
}
})When the body is a map (or an entity), FLIN automatically serializes it to JSON and sets the Content-Type: application/json header. You do not need to call JSON.stringify. You do not need to set headers manually. The common case -- sending JSON to a REST API -- is the default.
If you need a different content type, you can override it:
// Form data
response = http_post("https://api.example.com/login", {
body: "username=juste&password=secret",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
}
})// Raw text response = http_post("https://api.example.com/webhook", { body: "raw payload text", headers: { "Content-Type": "text/plain" } }) ```
The rule is simple: if you provide a map as the body, FLIN sends JSON. If you provide a string, FLIN sends it as-is with whatever Content-Type you specify.
Authentication Headers
Most API calls require authentication. FLIN's HTTP functions accept a headers map:
token = env("API_TOKEN")response = http_get("https://api.example.com/me", { headers: { "Authorization": "Bearer {token}" } })
user = response.json print("Logged in as: {user['name']}") ```
String interpolation works inside header values, so you can embed variables directly. The env() function reads environment variables -- another built-in that eliminates a dependency.
API Key Patterns
// Header-based API key
response = http_get("https://api.stripe.com/v1/charges", {
headers: { "Authorization": "Bearer {env('STRIPE_KEY')}" }
})// Query parameter API key key = env("WEATHER_API_KEY") response = http_get("https://api.weather.com/forecast?key={key}&city=Abidjan") ```
Timeouts and Retries
Production applications need timeouts (so a slow API does not hang your entire application) and retries (so transient network errors do not fail permanently). Both are built into the options:
response = http_post("https://api.payment.com/charge", {
body: { amount: 2500, currency: "XOF" },
timeout: 30.seconds,
retry: 3
})timeout accepts a duration value (using FLIN's natural duration syntax). If the request takes longer than the specified duration, it fails with a timeout error and response.ok is false.
retry specifies how many times to retry a failed request. Retries use exponential backoff: first retry after 1 second, second after 2 seconds, third after 4 seconds. Only network errors and 5xx responses trigger retries -- 4xx responses (client errors) are not retried because they would fail again with the same payload.
// Robust API call with full options
response = http_post("https://api.example.com/data", {
body: payload,
headers: {
"Authorization": "Bearer {token}",
"X-Request-Id": uuid()
},
timeout: 10.seconds,
retry: 2
}){if response.ok} data = response.json print("Success: {data['id']}") {else if response.status == 401} print("Authentication failed -- refresh token") {else if response.status == 429} print("Rate limited -- try again later") {else} print("Failed after retries: {response.status}") {/if} ```
PUT, PATCH, and DELETE
The remaining HTTP methods follow the same pattern:
// PUT - full replacement
response = http_put("https://api.example.com/users/42", {
body: {
name: "Juste Gnimavo",
email: "[email protected]",
role: "admin"
}
})// PATCH - partial update response = http_patch("https://api.example.com/users/42", { body: { role: "super_admin" } })
// DELETE response = http_delete("https://api.example.com/users/42")
// DELETE with body (some APIs require this) response = http_delete("https://api.example.com/batch", { body: { ids: [1, 2, 3] } }) ```
All five methods return the same response object structure. All five support the same options (body, headers, timeout, retry). There is no special behavior for any method -- the API is completely uniform.
Error Handling
HTTP requests can fail for many reasons: network errors, DNS failures, TLS certificate problems, timeouts. FLIN handles all of these by returning a response object with ok = false and a descriptive status:
response = http_get("https://unreachable.example.com"){if not response.ok} {if response.status == 0} // Network error (DNS failure, connection refused, etc.) print("Network error: {response.body}") {else if response.status >= 500} // Server error print("Server error: {response.status}") {else} // Client error (4xx) print("Client error: {response.status} - {response.body}") {/if} {/if} ```
Status code 0 is a FLIN convention for network-level failures (no HTTP response was received). This is different from JavaScript's fetch, which throws an exception for network errors, requiring a try-catch around every request. FLIN's approach is consistent: every http_* call returns a response object. You always check response.ok. The control flow is always visible in the code.
Real-World Example: Third-Party API Integration
Here is a complete example integrating with a payment API:
fn charge_customer(customer_id: text, amount: int, currency: text) {
response = http_post("https://api.payment.com/v1/charges", {
body: {
customer: customer_id,
amount: amount,
currency: currency,
description: "Order #{uuid().slice(0, 8)}"
},
headers: {
"Authorization": "Bearer {env('PAYMENT_SECRET_KEY')}",
"Idempotency-Key": uuid()
},
timeout: 30.seconds,
retry: 2
}){if response.ok} charge = response.json return { success: true, charge_id: charge["id"] } {else} error = response.json log_error("Payment failed: {response.status} - {error['message']}") return { success: false, error: error["message"] } {/if} }
// Usage in a view result = charge_customer("cus_abc123", 5000, "XOF")
{if result.success}
This is a production-ready payment integration in 30 lines. It handles authentication, idempotency keys, timeouts, retries, error responses, and user feedback. No HTTP library. No JSON parsing library. No environment variable library. All built-in.
Implementation: reqwest Under the Hood
FLIN's HTTP client is built on Rust's reqwest crate, the most popular HTTP client in the Rust ecosystem. reqwest uses hyper for HTTP/1.1 and HTTP/2 support, rustls for TLS, and tokio for async I/O.
The built-in functions are thin wrappers that translate FLIN values to reqwest calls:
fn builtin_http_post(vm: &mut Vm, args: &[Value]) -> Result<Value, VmError> {
let url = vm.get_string(args[0])?;
let options = if args.len() > 1 { vm.get_map(args[1])? } else { Map::new() };let client = reqwest::blocking::Client::builder() .timeout(extract_timeout(&options)) .build() .map_err(|e| VmError::HttpError(e.to_string()))?;
let mut request = client.post(url);
// Set headers if let Some(headers) = options.get("headers") { for (key, value) in vm.get_map(headers)?.iter() { request = request.header(key, value); } }
// Set body (auto-serialize maps to JSON) if let Some(body) = options.get("body") { match body { Value::Map(_) => request = request.json(&serialize_to_json(vm, body)?), Value::String(s) => request = request.body(s.clone()), _ => request = request.body(vm.value_to_string(body)?), } }
// Execute with retry logic let response = execute_with_retry(request, extract_retry_count(&options))?;
// Build response map Ok(build_response_value(vm, response)) } ```
The blocking client is used because FLIN's VM is currently single-threaded. Async HTTP support is planned for a future release, where http_get will become an async operation that the VM can yield on while waiting for the response.
What We Intentionally Excluded
The HTTP client is deliberately simple. We excluded features that add complexity without benefiting 90% of use cases:
Cookie jars. Automatic cookie management across requests adds state that is hard to debug. If you need cookies, pass them as headers explicitly.
File upload. Multipart form data is complex and rarely needed in API-to-API communication. File uploads in FLIN are handled by the view system and entity system, not the HTTP client.
WebSocket support. WebSockets are a different protocol with different semantics. They will be a separate built-in (ws_connect) when implemented.
Request interceptors. Middleware chains for logging, authentication, and transformation are an antipattern in a language that values explicitness. If you need to add an auth header to every request, write a wrapper function. It is five lines, it is obvious, and it does not require understanding a middleware pipeline.
Streaming responses. Server-sent events and streaming responses require async support. They are planned for the async runtime release.
The HTTP client does one thing well: make synchronous HTTP requests with JSON bodies and return structured responses. For 90% of web application API calls, that is exactly what you need.
Five Functions, Zero Dependencies
FLIN's HTTP client replaces:
- JavaScript:
fetchAPI +AbortController+JSON.stringify+ error handling boilerplate - Python:
requestslibrary (31 million downloads per week) - Go:
net/http+json.Marshal+ioutil.ReadAll - Ruby:
net/httporfaradayorhttparty
Five functions. One options format. One response format. Every HTTP method a web application uses, available from the first line of code with zero configuration.
---
This is Part 75 of the "How We Built FLIN" series, documenting how a CEO in Abidjan and an AI CTO built an HTTP client into a programming language.
Series Navigation: - [74] Time and Timezone Functions - [75] HTTP Client Built Into the Language (you are here) - [76] Security Functions: Crypto, JWT, Argon2 - [77] Introspection and Reflection at Runtime