2026-01-26 05:12:14 +01:00
{# retoor < retoor @ molodetz . nl > #}
{% extends 'page.html' %}
2026-01-24 23:40:22 +01:00
2026-01-26 05:12:14 +01:00
{% set page_title = "Building an HTTP Client" %}
{% set breadcrumb = [{"url": "tutorials/index.html", "title": "Tutorials"}, {"title": "HTTP Client"}] %}
{% set prev_page = {"url": "tutorials/index.html", "title": "Tutorials"} %}
{% set next_page = {"url": "tutorials/websocket-chat.html", "title": "WebSocket Chat"} %}
{% block article %}
2026-01-24 23:40:22 +01:00
< h1 > Building an HTTP Client</ h1 >
< p > In this tutorial, you will learn how to build a REST API client using Wren-CLI's < code > http</ code > and < code > json</ code > modules. By the end, you will have a reusable API client class that can interact with any JSON REST API.</ p >
< h2 > What You Will Learn</ h2 >
< ul >
< li > Making GET, POST, PUT, and DELETE requests</ li >
< li > Parsing JSON responses</ li >
< li > Handling HTTP errors</ li >
< li > Working with request headers</ li >
< li > Building a reusable API client class</ li >
</ ul >
< h2 > Step 1: Your First HTTP Request</ h2 >
< p > Let's start with a simple GET request. Create a file called < code > http_client.wren</ code > :</ p >
< pre >< code > import "http" for Http
var response = Http.get("https://jsonplaceholder.typicode.com/posts/1")
System.print("Status: %(response.statusCode)")
System.print("Body: %(response.body)")</ code ></ pre >
< p > Run it:</ p >
< pre >< code > $ wren_cli http_client.wren
Status: 200
Body: {
"userId": 1,
"id": 1,
"title": "sunt aut facere...",
"body": "quia et suscipit..."
}</ code ></ pre >
< h2 > Step 2: Parsing JSON Responses</ h2 >
< p > Raw JSON strings are not very useful. Let's parse them into Wren objects:</ p >
< pre >< code > import "http" for Http
import "json" for Json
var response = Http.get("https://jsonplaceholder.typicode.com/posts/1")
if (response.statusCode == 200) {
var post = Json.parse(response.body)
System.print("Title: %(post["title"])")
System.print("User ID: %(post["userId"])")
} else {
System.print("Error: %(response.statusCode)")
}</ code ></ pre >
< p > The < code > HttpResponse</ code > class also provides a convenient < code > json</ code > property:</ p >
< pre >< code > import "http" for Http
var response = Http.get("https://jsonplaceholder.typicode.com/posts/1")
var post = response.json
System.print("Title: %(post["title"])")</ code ></ pre >
< h2 > Step 3: Fetching Lists</ h2 >
< p > APIs often return arrays of objects. Let's fetch multiple posts:</ p >
< pre >< code > import "http" for Http
var response = Http.get("https://jsonplaceholder.typicode.com/posts")
var posts = response.json
System.print("Found %(posts.count) posts\n")
for (i in 0...5) {
var post = posts[i]
System.print("%(post["id"]). %(post["title"])")
}</ code ></ pre >
< h2 > Step 4: Making POST Requests</ h2 >
< p > To create resources, use POST with a JSON body:</ p >
< pre >< code > import "http" for Http
import "json" for Json
var newPost = {
"title": "My New Post",
"body": "This is the content of my post.",
"userId": 1
}
var response = Http.post(
"https://jsonplaceholder.typicode.com/posts",
Json.stringify(newPost),
{"Content-Type": "application/json"}
)
System.print("Status: %(response.statusCode)")
System.print("Created post with ID: %(response.json["id"])")</ code ></ pre >
< h2 > Step 5: PUT and DELETE Requests</ h2 >
< p > Update and delete resources with PUT and DELETE:</ p >
< pre >< code > import "http" for Http
import "json" for Json
var updatedPost = {
"id": 1,
"title": "Updated Title",
"body": "Updated content.",
"userId": 1
}
var putResponse = Http.put(
"https://jsonplaceholder.typicode.com/posts/1",
Json.stringify(updatedPost),
{"Content-Type": "application/json"}
)
System.print("PUT Status: %(putResponse.statusCode)")
var deleteResponse = Http.delete("https://jsonplaceholder.typicode.com/posts/1")
System.print("DELETE Status: %(deleteResponse.statusCode)")</ code ></ pre >
< h2 > Step 6: Building an API Client Class</ h2 >
< p > Let's create a reusable API client that encapsulates all these patterns:</ p >
< pre >< code > import "http" for Http
import "json" for Json
class ApiClient {
construct new(baseUrl) {
_baseUrl = baseUrl
_headers = {"Content-Type": "application/json"}
}
headers { _headers }
headers=(value) { _headers = value }
setHeader(name, value) {
_headers[name] = value
}
get(path) {
var response = Http.get(_baseUrl + path, _headers)
return handleResponse(response)
}
post(path, data) {
var body = Json.stringify(data)
var response = Http.post(_baseUrl + path, body, _headers)
return handleResponse(response)
}
put(path, data) {
var body = Json.stringify(data)
var response = Http.put(_baseUrl + path, body, _headers)
return handleResponse(response)
}
delete(path) {
var response = Http.delete(_baseUrl + path, _headers)
return handleResponse(response)
}
handleResponse(response) {
if (response.statusCode >= 200 && response.statusCode < 300 ) {
if ( response . body . count > 0) {
return response.json
}
return null
}
Fiber.abort("API Error: %(response.statusCode)")
}
}
var api = ApiClient.new("https://jsonplaceholder.typicode.com")
var posts = api.get("/posts")
System.print("Fetched %(posts.count) posts")
var newPost = api.post("/posts", {
"title": "Hello from Wren",
"body": "Created with ApiClient",
"userId": 1
})
System.print("Created post: %(newPost["id"])")</ code ></ pre >
< h2 > Step 7: Adding Authentication</ h2 >
< p > Many APIs require authentication. Add support for API keys and bearer tokens:</ p >
< pre >< code > import "http" for Http
import "json" for Json
import "base64" for Base64
class ApiClient {
construct new(baseUrl) {
_baseUrl = baseUrl
_headers = {"Content-Type": "application/json"}
}
setApiKey(key) {
_headers["X-API-Key"] = key
}
setBearerToken(token) {
_headers["Authorization"] = "Bearer %(token)"
}
setBasicAuth(username, password) {
var credentials = Base64.encode("%(username):%(password)")
_headers["Authorization"] = "Basic %(credentials)"
}
get(path) {
var response = Http.get(_baseUrl + path, _headers)
return handleResponse(response)
}
handleResponse(response) {
if (response.statusCode == 401) {
Fiber.abort("Authentication failed")
}
if (response.statusCode == 403) {
Fiber.abort("Access forbidden")
}
if (response.statusCode >= 200 && response.statusCode < 300 ) {
return response . json
}
Fiber . abort (" API Error: %( response . statusCode )")
}
}
var api = ApiClient.new("https://api.example.com")
api . setBearerToken (" your-auth-token ")
var data = api.get("/protected/resource")</code ></ pre >
< h2 > Step 8: Error Handling</ h2 >
< p > Proper error handling makes your client robust:</ p >
< pre >< code > import "http" for Http
import "json" for Json
class ApiError {
construct new(statusCode, message) {
_statusCode = statusCode
_message = message
}
statusCode { _statusCode }
message { _message }
toString { "ApiError %(statusCode): %(message)" }
}
class ApiClient {
construct new(baseUrl) {
_baseUrl = baseUrl
_headers = {"Content-Type": "application/json"}
}
get(path) {
var fiber = Fiber.new {
return Http.get(_baseUrl + path, _headers)
}
var response = fiber.try()
if (fiber.error) {
return {"error": ApiError.new(0, fiber.error)}
}
return handleResponse(response)
}
handleResponse(response) {
if (response.statusCode >= 200 && response.statusCode < 300 ) {
return {" data " : response . json }
}
var message = "Unknown error"
var fiber = Fiber.new { response . json [" message "] }
var apiMessage = fiber.try()
if (! fiber . error && apiMessage ) {
message = apiMessage
}
return {" error " : ApiError . new ( response . statusCode , message )}
}
}
var api = ApiClient.new("https://jsonplaceholder.typicode.com")
var result = api.get("/posts/1")
if ( result [" error "]) {
System . print (" Error: %( result [" error "])")
} else {
System . print (" Success: %( result [" data "][" title "])")
}</ code ></ pre >
2026-01-25 19:02:02 +01:00
< h2 > Step 9: Concurrent HTTP Requests</ h2 >
< p > When you need to fetch data from multiple endpoints, sequential requests can be slow. Use < code > async</ code > and < code > await</ code > to run requests concurrently:</ p >
< pre >< code > import "http" for Http
import "scheduler" for Scheduler, Future
import "json" for Json
var fetchJson = async { |url|
var response = Http.get(url)
return response.json
}
// SEQUENTIAL: Each request waits for the previous one
System.print("--- Sequential requests ---")
var user = await fetchJson("https://jsonplaceholder.typicode.com/users/1")
var posts = await fetchJson("https://jsonplaceholder.typicode.com/posts?userId=1")
var todos = await fetchJson("https://jsonplaceholder.typicode.com/todos?userId=1")
System.print("User: %(user["name"])")
System.print("Posts: %(posts.count)")
System.print("Todos: %(todos.count)")</ code ></ pre >
< p > For concurrent execution, use < code > .call()</ code > to start requests without waiting, then < code > await</ code > the results:</ p >
< pre >< code > import "http" for Http
import "scheduler" for Scheduler, Future
import "json" for Json
var fetchJson = async { |url|
var response = Http.get(url)
return response.json
}
// CONCURRENT: All requests start at once
System.print("--- Concurrent requests ---")
var f1 = fetchJson.call("https://jsonplaceholder.typicode.com/users/1")
var f2 = fetchJson.call("https://jsonplaceholder.typicode.com/posts?userId=1")
var f3 = fetchJson.call("https://jsonplaceholder.typicode.com/todos?userId=1")
// Wait for results (requests run in parallel)
var user = await f1
var posts = await f2
var todos = await f3
System.print("User: %(user["name"])")
System.print("Posts: %(posts.count)")
System.print("Todos: %(todos.count)")</ code ></ pre >
< h3 > Batch Fetching with Concurrent Requests</ h3 >
< p > For fetching multiple URLs dynamically, create futures in a loop:</ p >
< pre >< code > import "http" for Http
import "scheduler" for Scheduler, Future
import "json" for Json
var fetchJson = async { |url|
var response = Http.get(url)
return response.json
}
var userIds = [1, 2, 3, 4, 5]
// Start all requests concurrently
var futures = []
for (id in userIds) {
futures.add(fetchJson.call("https://jsonplaceholder.typicode.com/users/%(id)"))
}
// Collect results
var users = []
for (f in futures) {
users.add(await f)
}
System.print("Fetched %(users.count) users:")
for (user in users) {
System.print(" - %(user["name"]) (%(user["email"]))")
}</ code ></ pre >
< div class = "admonition tip" >
< div class = "admonition-title" > Direct Calling vs .call()</ div >
< p > Use < code > await fn(args)</ code > for sequential execution (waits immediately). Use < code > fn.call(args)</ code > to start without waiting, enabling concurrent execution.</ p >
</ div >
2026-01-24 23:40:22 +01:00
< h2 > Complete Example</ h2 >
< p > Here is a complete, production-ready API client:</ p >
< pre >< code > import "http" for Http
import "json" for Json
import "base64" for Base64
class ApiClient {
construct new(baseUrl) {
_baseUrl = baseUrl
_headers = {"Content-Type": "application/json"}
_timeout = 30000
}
baseUrl { _baseUrl }
headers { _headers }
setHeader(name, value) { _headers[name] = value }
removeHeader(name) { _headers.remove(name) }
setBearerToken(token) {
_headers["Authorization"] = "Bearer %(token)"
}
setBasicAuth(username, password) {
var credentials = Base64.encode("%(username):%(password)")
_headers["Authorization"] = "Basic %(credentials)"
}
get(path) { request("GET", path, null) }
post(path, data) { request("POST", path, data) }
put(path, data) { request("PUT", path, data) }
patch(path, data) { request("PATCH", path, data) }
delete(path) { request("DELETE", path, null) }
request(method, path, data) {
var url = _baseUrl + path
var body = data ? Json.stringify(data) : ""
var response
if (method == "GET") {
response = Http.get(url, _headers)
} else if (method == "POST") {
response = Http.post(url, body, _headers)
} else if (method == "PUT") {
response = Http.put(url, body, _headers)
} else if (method == "PATCH") {
response = Http.patch(url, body, _headers)
} else if (method == "DELETE") {
response = Http.delete(url, _headers)
}
return parseResponse(response)
}
parseResponse(response) {
var result = {
"status": response.statusCode,
"headers": response.headers,
"ok": response.statusCode >= 200 && response.statusCode < 300
}
if ( response . body . count > 0) {
var fiber = Fiber.new { Json.parse(response.body) }
var data = fiber.try()
result["data"] = fiber.error ? response.body : data
}
return result
}
}
System.print("=== API Client Demo ===\n")
var api = ApiClient.new("https://jsonplaceholder.typicode.com")
System.print("--- Fetching posts ---")
var result = api.get("/posts")
if (result["ok"]) {
System.print("Found %(result["data"].count) posts")
System.print("First post: %(result["data"][0]["title"])")
}
System.print("\n--- Creating post ---")
result = api.post("/posts", {
"title": "Created with Wren-CLI",
"body": "This is a test post",
"userId": 1
})
if (result["ok"]) {
System.print("Created post ID: %(result["data"]["id"])")
}
System.print("\n--- Updating post ---")
result = api.put("/posts/1", {
"id": 1,
"title": "Updated Title",
"body": "Updated body",
"userId": 1
})
System.print("Update status: %(result["status"])")
System.print("\n--- Deleting post ---")
result = api.delete("/posts/1")
System.print("Delete status: %(result["status"])")</ code ></ pre >
< div class = "admonition tip" >
< div class = "admonition-title" > Tip</ div >
< p > Save the < code > ApiClient</ code > class in a separate file like < code > api_client.wren</ code > and import it in your projects for reuse.</ p >
</ div >
< h2 > Next Steps</ h2 >
< ul >
< li > Learn about < a href = "../api/http.html" > HTTP module</ a > features in detail</ li >
< li > Explore < a href = "../api/json.html" > JSON module</ a > for advanced parsing</ li >
< li > Build a < a href = "websocket-chat.html" > WebSocket chat application</ a ></ li >
</ ul >
2026-01-26 05:12:14 +01:00
{% endblock %}