Some Personal Zig Projects

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!
}
Tags: Zig CGI HTTP