Zig HTTP Client and Server Examples for ESP-IDF: A Complete Implementation Guide
The zig-esp-idf-sample repository provides idiomatic Zig wrappers around ESP-IDF's HTTP components, implementing both an HTTP server with URI handlers and an HTTP client with synchronous request capabilities.
The zig-esp-idf-sample repository serves as the definitive reference for implementing Zig HTTP client and server examples for ESP-IDF microcontrollers on the ESP32 platform. By combining Zig's error handling and compile-time features with ESP-IDF's battle-tested C APIs, developers can build robust embedded web services and REST clients without sacrificing memory safety or performance.
Architecture Overview
The codebase abstracts ESP-IDF's HTTP components through thin wrappers defined in imports/http.zig. These wrappers convert low-level C error codes into Zig error unions while preserving the full functionality of the underlying esp_http_server and esp_http_client libraries.
HTTP Server Architecture
The server implementation exposes idf.http.Server for lifecycle management and idf.http.Server.Response for output handling. These wrap the esp_httpd_* API family, allowing Zig code to register URI handlers and send responses using native error handling constructs.
HTTP Client Architecture
The client side encapsulates esp_http_client_* functions within the idf.http.Client struct. All methods return Zig errors via idf.err.espCheckError, which translates ESP-IDF error codes (like ESP_FAIL) into catchable Zig errors.
Building an HTTP Server in Zig
The complete server implementation resides in main/examples/http-server.zig, demonstrating Wi-Fi initialization, server configuration, and URI handler registration.
Configuring the Server
Server initialization requires populating a sys.httpd_config_t struct to define task parameters and resource limits:
var config = std.mem.zeroes(sys.httpd_config_t);
config.server_port = 80;
config.stack_size = 4096;
config.task_priority = 5;
config.max_uri_handlers = 8;
Registering URI Handlers
Handlers must use the C calling convention and accept a sys.httpd_req_t pointer. The example implements two endpoints: a static HTML root page and a JSON API:
export fn handleRoot(req: [*c]sys.httpd_req_t) callconv(.c) sys.esp_err_t {
idf.http.Server.Response.sendStr(req, index_html) catch |err| {
log.err("sendStr: {s}", .{@errorName(err)});
return sys.ESP_FAIL;
};
return sys.ESP_OK;
}
export fn handleApiHello(req: [*c]sys.httpd_req_t) callconv(.c) sys.esp_err_t {
idf.http.Server.Response.setType(req, "application/json") catch {};
idf.http.Server.Response.sendStr(req,
"{\"message\":\"Hello from Zig!\"}") catch |err| {
log.err("sendStr: {s}", .{@errorName(err)});
return sys.ESP_FAIL;
};
return sys.ESP_OK;
}
Starting the Server
The startHttpServer() function initializes the server and registers endpoints using idf.http.Server.registerUri():
fn startHttpServer() !void {
var config = std.mem.zeroes(sys.httpd_config_t);
config.server_port = 80;
config.stack_size = 4096;
config.task_priority = 5;
config.max_uri_handlers = 8;
const server = try idf.http.Server.start(&config);
const root_uri = sys.httpd_uri_t{
.uri = "/",
.method = sys.HTTP_GET,
.handler = &handleRoot,
.user_ctx = null,
};
try idf.http.Server.registerUri(server, &root_uri);
const api_uri = sys.httpd_uri_t{
.uri = "/api/hello",
.method = sys.HTTP_GET,
.handler = &handleApiHello,
.user_ctx = null,
};
try idf.http.Server.registerUri(server, &api_uri);
log.info("HTTP server started on port 80", .{});
}
The server is invoked from app_main() in main/app.zig after Wi-Fi connection establishment (lines 78–122 of http-server.zig).
Implementing an HTTP Client
The client implementation in imports/http.zig (lines 165–236) provides synchronous request capabilities with streaming support.
Client Initialization and Configuration
Initialize the client with an esp_http_client_config_t struct containing the target URL and optional event handlers:
pub const Client = struct {
handle: sys.esp_http_client_handle_t = null,
pub fn init(cfg: *const sys.esp_http_client_config_t) Client {
return .{ .handle = sys.esp_http_client_init(cfg) };
}
pub fn setUrl(self: *Client, url: [*:0]const u8) !void {
try errors.espCheckError(sys.esp_http_client_set_url(self.handle, url));
}
};
Performing Requests and Reading Responses
The client supports synchronous execution via perform() followed by status code inspection and body reading:
pub fn perform(self: *Client) !void {
try errors.espCheckError(sys.esp_http_client_perform(self.handle));
}
pub fn statusCode(self: *Client) u32 {
return sys.esp_http_client_get_status_code(self.handle);
}
pub fn read(self: *Client, buf: []u8) usize {
return @intCast(sys.esp_http_client_read(self.handle, buf.ptr, @intCast(buf.len)));
}
pub fn deinit(self: *Client) !void {
try errors.espCheckError(sys.esp_http_client_cleanup(self.handle));
}
Complete Client Usage Example
A typical GET request implementation looks like this:
pub fn fetchExample() !void {
var cfg = sys.esp_http_client_config_t{
.url = "http://example.com",
.event_handler = null,
.user_data = null,
.disable_auto_redirect = false,
};
var client = http.Client.init(&cfg);
defer client.deinit() catch {};
try client.setUrl("http://example.com");
try client.perform();
const status = client.statusCode();
std.debug.print("Status = {}\n", .{status});
var buf: [1024]u8 = undefined;
const len = client.read(&buf);
std.debug.print("Body ({}) = {s}\n", .{ len, buf[0..len] });
}
Advanced Pattern: HTTP Proxy Implementation
You can combine both components to create a proxy endpoint that fetches remote data within a server handler:
export fn handleProxy(req: [*c]sys.httpd_req_t) callconv(.c) sys.esp_err_t {
var cfg = sys.esp_http_client_config_t{
.url = "http://worldtimeapi.org/api/ip",
.event_handler = null,
.user_data = null,
.disable_auto_redirect = false,
};
var client = idf.http.Client.init(&cfg);
defer client.deinit() catch {};
client.perform() catch |e| {
idf.http.Server.Response.send500(req) catch {};
return sys.ESP_FAIL;
};
var body: [512]u8 = undefined;
const n = client.read(&body);
try idf.http.Server.Response.setType(req, "application/json");
try idf.http.Server.Response.send(req, body[0..n]);
return sys.ESP_OK;
}
Register this handler like any other URI to transform the ESP32 into a lightweight HTTP proxy.
Project Structure and Key Files
Understanding the repository layout is essential for extending these examples:
main/examples/http-server.zig– Full server implementation with Wi-Fi initialization and URI handlersimports/http.zig– Zig wrappers for bothidf.http.Serverandidf.http.Clientimports/wifi.zig– Wi-Fi convenience helpers for connecting to access pointsmain/app.zig– Application entry point (app_main) that orchestrates subsystem initializationdocs/getting-started.md– Build system configuration and toolchain setup instructions
Summary
- The zig-esp-idf-sample repository demonstrates production-ready Zig HTTP client and server examples for ESP-IDF using thin wrappers over ESP-IDF C APIs.
- The HTTP server implementation in
imports/http.zigprovidesidf.http.Server.start(),registerUri(), andResponse.sendStr()for handling GET requests. - The HTTP client exposes
idf.http.Clientwithinit(),perform(), andread()methods for synchronous HTTP requests. - All ESP-IDF error codes are automatically converted to Zig error unions via
idf.err.espCheckError, enabling idiomatictryandcatcherror handling. - The server configuration uses
sys.httpd_config_tto define stack size (4096 bytes), task priority (5), and maximum URI handlers (8). - Both components can be combined to implement proxy patterns or API gateways on ESP32 hardware.
Frequently Asked Questions
How do I change the HTTP server port in the ESP-IDF Zig implementation?
Modify the server_port field in the sys.httpd_config_t struct before calling idf.http.Server.start(). The default configuration sets config.server_port = 80, but you can specify any valid port number (e.g., 8080) for development or deployment scenarios requiring non-privileged ports.
Can the Zig HTTP client handle POST requests with request bodies?
Yes. After initializing the client with idf.http.Client.init(), use sys.esp_http_client_set_method() to set the method to HTTP_METHOD_POST, then write the request body using sys.esp_http_client_write() before calling client.perform(). The wrapper in imports/http.zig exposes these underlying ESP-IDF functions for full HTTP method support.
How does error handling work between Zig and ESP-IDF in these examples?
The idf.err.espCheckError function (used throughout imports/http.zig) translates ESP-IDF C error codes (like ESP_FAIL or ESP_ERR_NO_MEM) into Zig error unions. This allows Zig code to use try for propagation and catch for specific error handling, while C handlers in server callbacks return sys.esp_err_t values directly to the ESP-IDF HTTP daemon.
What is the minimum stack size required for the HTTP server task?
According to the implementation in main/examples/http-server.zig, the server task requires at least 4096 bytes of stack (config.stack_size = 4096). This accommodates the HTTP daemon's internal buffers and the Zig handler function call frames. Increase this value if your URI handlers perform heavy allocations or complex processing.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →