HTTP Client Lands: Luma Talks to the Web

HTTP Client Lands: Luma Talks to the Web

February 16, 2026·
Luma Core Team

Every modern program talks to an API. Weather data, payment gateways, authentication services, third-party integrations — HTTP is the lingua franca of the internet. Luma could already serve HTTP with server.*, but it couldn’t make requests. That gap closed today.

Luma now has a built-in HTTP client with a builder pattern: create a request, configure it step by step, execute.

The simplest case

res = http("https://api.example.com/users").get()
print(res.status)  // 200
print(res.body)    // JSON string

One line to make a GET request. http() creates the client, .get() fires it. The response gives you status, body, and headers — everything you need.

Builder pattern

For more control, build the request step by step:

h: http = http("https://api.example.com/users")
h.header("Authorization", "Bearer token123")
h.header("Accept", "application/json")
h.timeout(10)
res = h.post()

This follows the same pattern as buffer() and tcp.connect() — create an object, configure it, use it. No surprise constructors, no magic options objects.

POST with JSON body

Pass a map to .body() and Luma serializes it to JSON automatically. The Content-Type header is set for you.

h: http = http("https://api.example.com/users")
h.body({"name": "Alice", "age": 30, "active": true})
res = h.post()
print(res.body)

No json.encode() call. No manual Content-Type. Just pass the data.

POST with form fields

For classic form submissions, use .field():

h: http = http("https://example.com/login")
h.field("username", "alice")
h.field("password", "secret")
res = h.post()

Fields are URL-encoded automatically. Content-Type: application/x-www-form-urlencoded is set for you. .field() and .body() are mutually exclusive — use one or the other.

Raw string body

When you need full control over the body, pass a string to .body():

h: http = http("https://example.com/api")
h.header("Content-Type", "application/xml")
h.body("<user><name>Alice</name></user>")
res = h.post()

String bodies don’t auto-set Content-Type — you control the header yourself.

All HTTP methods

Five methods cover the standard REST operations:

res = h.get()
res = h.post()
res = h.put()
res = h.delete()
res = h.patch()

Reading the response

The response object has three fields:

res = http("https://api.example.com/data").get()

print(res.status)                    // 200
print(res.body)                      // response body as string
ct = res.headers["Content-Type"]     // "application/json"
  • res.status — HTTP status code as integer
  • res.body — response body as string
  • res.headers — response headers as {str: str} map

HTTP 4xx and 5xx responses are valid responses, not errors. You get the status code and body — your code decides what to do with them.

Timeouts

Default timeout is 30 seconds. Override it with .timeout():

h: http = http("https://slow-api.example.com/data")
h.timeout(5)  // 5 seconds
res = h.get()

Error handling

Network errors and timeouts panic — consistent with how TCP, file I/O, and every other I/O operation works in Luma. Catch them with .or():

h: http = http("https://unreachable.example.com")
h.timeout(2)
res = h.get().or(err -> {
    print("Request failed: ${err}")
})

This is the same .or() pattern you already use for file operations, database queries, and TCP connections. No new error handling concept to learn.

A real example: calling a REST API

Here’s a complete program that creates a user via a REST API and prints the result:

// Create user
h: http = http("https://jsonplaceholder.typicode.com/users")
h.header("Accept", "application/json")
h.body({
    "name": "Alice Johnson",
    "username": "alice",
    "email": "alice@example.com"
})
res = h.post()

if res.status == 201 {
    print("User created!")
    print(res.body)
} else {
    print("Failed with status: ${res.status}")
}

No imports. No dependency installation. No HTTP client initialization boilerplate. Just build the request and send it.

Design decisions

Why a builder pattern? Because HTTP requests have many optional parts — headers, body, timeout, authentication. A builder lets you configure only what you need without overloading a single function with dozens of parameters.

Why http() and not http.new()? Consistency. Luma uses buffer(), file(), cli(), datetime() — constructors are bare function calls. The HTTP client follows the same convention.

Why auto-serialize maps? Because the most common HTTP body is JSON, and making developers call json.encode() every time adds friction without adding clarity. If you pass a map, it becomes JSON. If you pass a string, it stays a string. The intent is obvious from the argument type.

Why panic on network errors? Because that’s what Luma does for all I/O. File not found? Panic. TCP connection refused? Panic. DNS resolution failed? Panic. The developer catches it with .or() when they want to handle the failure. This keeps the happy path clean and the error path explicit.

What this unlocks

With the HTTP client in place, Luma programs can now:

  • Consume REST APIs and microservices
  • Integrate with third-party services (Stripe, Twilio, SendGrid, etc.)
  • Build API-to-API bridges and webhooks
  • Fetch remote data for processing pipelines
  • Health-check external services
  • Interact with any HTTP-based protocol

Combined with the web server (server.*), JSON (json.encode / json.decode), and database support (db.*), Luma now covers the full request cycle: receive a request, query a database, call an external API, return a response. That’s a complete backend.

Full API reference

Method Description
http(url) Create an HTTP client for the given URL
.header(key, value) Set a request header
.field(key, value) Add a form field (URL-encoded)
.body(map) Set JSON body (auto-serialized)
.body(string) Set raw string body
.timeout(seconds) Set timeout (default: 30s)
.get() Execute GET request
.post() Execute POST request
.put() Execute PUT request
.delete() Execute DELETE request
.patch() Execute PATCH request

Response fields:

Field Type Description
res.status int HTTP status code
res.body str Response body
res.headers {str: str} Response headers

The HTTP client is available now. No installation, no imports — it’s built into the language.

Last updated on