Table of contents
Context
Since the end of 2025, I’m really interested in the Zig programing language. I’m coming from a C programing language background, more than 20 years of experience. And recurrently, I’ve come to code with the Web, HTML responses, in mind. I’ve done this with multiple programing languages, and since Zig has got a lot of my attention, and having done the same with C, I wanted to be able to have some “interfaces” to these past experiences but with Zig. And at the time I was coding these “interfaces”, they were not existing directly in the Zig Standard Library.
I do not like very complications, or too complicated matters, I’m really relying on this concept in french:
- “Ce que l’on conçoit bien s’énonce clairement, Et les mots pour le dire arrivent aisément.”
- Translated: “What is clearly conceived is clearly expressed, and the words to say it come easily.”
- which is why I love the “KISS!” concept: “Keep It Simple, Stupid!”
So I created two Zig packages that can help me:
- code and interface simply with the Common Gateway Interface (CGI)
- to be able to work with HTML forms, especialy those with “
multipart/form-data”
zplocgi
Project URL: https://codeberg.org/y0m/zplocgi/
This Zig package, although nightmarely named, gets its name from a C library I’ve already coded in the past, which give also some key to the Common Gateway Interface, aka. CGI, RFC 3875. The C library is called “aplocgi”, the “aplo-” sufix is coming from the greek to “simple”. So this is supposed to be a “Simple CGI” interface with the Zig programing language.
And since it has to be simple, here is a sample of Zig code, some source code removed for clarity:
const std = @import("std");
const Request = @import("zplocgi").Request;
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
// Create the incoming HTTP request from the CGI
var req: Request = try .init(allocator, init.environ_map);
defer req.deinit();
// Define stdout
var stdout_buffer: [4096]u8 = undefined;
var stdout_writer: std.Io.File.Writer = .init(.stdout(), init.io, &stdout_buffer);
const stdout = &stdout_writer.interface;
// [...] source code removed for clarity
// Finally, print out the template buffer
try Request.respond(stdout, buffer.items, .{
// HTTP Status Code 200 OK
.status = .ok,
.extra_headers = &.{
// The content type is plain text, no HTML
.{ .name = "Content-Type", .value = "text/plain" },
},
});
}
zformdata
Project URL: https://codeberg.org/y0m/zformdata/
This Zig package gives a way to interact with HTML forms, either with simple HTML input elements like text, select, etc.,
or with more “complex” like file, which is mostly used with the multipart/form-data encoding type, described in the
RFC 7578.
It’s using Zig tagged enum to store possible values, either in the form of simple text, or as “file” contents sent with
multipart/form-data. The project itself contains some Zig unit tests.
Although there is a small sample of code inside this project, I’ve also got a repository that’s holding a code sample
to interfacing a HTML form with multipart/form-data encoding type, and some inputs named “toto”, AND using zplocgi as well.
This sample can be found here:
Sample code:
const std = @import("std");
const Io = std.Io;
const Request = @import("zplocgi").Request;
const FormData = @import("zformdata").FormData;
pub fn main(init: std.process.Init) !void {
// In order to do I/O operations need an `Io` instance.
const io = init.io;
// Stdout is for the actual output of your application, for example if you
// are implementing gzip, then only the compressed bytes should be sent to
// stdout, not any debugging messages.
var stdout_buffer: [1024]u8 = undefined;
var stdout_file_writer: Io.File.Writer = .init(.stdout(), io, &stdout_buffer);
const stdout_writer = &stdout_file_writer.interface;
// Defining stdin since `multipart/form-data` will be there
var stdin_buffer: [4096]u8 = undefined;
var stdin_reader: Io.File.Reader = .init(.stdin(), io, &stdin_buffer);
const stdin = &stdin_reader.interface;
// Getting the CGI informations
const req: Request = try .init(init.gpa, init.environ_map);
var form: FormData = try .parse(
init.gpa,
.init(
.POST,
req.headers.get("QUERY_STRING").?,
req.content_type,
req.content_length,
stdin,
),
null,
);
// Template buffer
var buffer: std.ArrayList(u8) = try .initCapacity(init.gpa, 4096);
defer buffer.deinit(init.gpa);
try buffer.print(init.gpa, "Form field toto\n", .{});
const toto = form.getFormField("toto") orelse return error.UhOh;
// Listing form fields/inputs
for (toto) |item| {
switch (item) {
// can be either a text value
.value => |value| try buffer.print(init.gpa, " value: {s}\n", .{value}),
// or a file content and informations
.file => |file| {
try buffer.print(init.gpa, " filename: {s}\n", .{file.filename});
try buffer.print(init.gpa, " content type: {s}\n", .{file.content_type});
try buffer.print(init.gpa, " content:\n{s}\n", .{file.content});
},
}
}
// Print out the template buffer
try Request.respond(stdout_writer, buffer.items, .{
.status = .ok,
.extra_headers = &.{
.{ .name = "Content-Type", .value = "text/plain; charset=utf-8" },
},
});
try stdout_writer.flush(); // Don't forget to flush!
}