Skip to content

Getting Started

This guide shows how to write a small HTTP server with Dusty. If you want to use the HTTP client, see Making Requests instead.

Setup

We'll start from an empty project. You need Zig 0.16 or 0.17.

Create the project and add Dusty as a dependency:

$ zig init
$ zig fetch --save "git+https://github.com/lalinsky/dusty"

TODO

This installs Dusty from the main branch. Switch to a release tag once the next version is out.

Then open build.zig and add the module to your executable, after the exe definition:

const dusty_dep = b.dependency("dusty", .{
    .target = target,
    .optimize = optimize,
});

exe.root_module.addImport("dusty", dusty_dep.module("dusty"));

See Build Options for how to configure the package.

Hello World

Put this into src/main.zig:

const std = @import("std");
const http = @import("dusty");

const App = struct {
    greeting: []const u8,
};

fn index(app: *App, res: *http.Response) !void {
    try res.print(.text, "{s}, world!\n", .{app.greeting});
}

fn hello(app: *App, name: http.Param("name", []const u8), res: *http.Response) !void {
    try res.print(.text, "{s}, {s}!\n", .{ app.greeting, name.value });
}

pub fn main(init: std.process.Init) !void {
    var app: App = .{ .greeting = "Hello" };

    var server = http.Server(App).init(init.gpa, init.io, .{
        .listeners = &.{.{
            .address = .{ .ip = try std.Io.net.IpAddress.parse("127.0.0.1", 8080) },
        }},
    }, &app);
    defer server.deinit();

    server.router.get("/", index);
    server.router.get("/hello/:name", hello);

    try server.run();
}

Now build and run it:

$ zig build run
info(dusty): Listening on 127.0.0.1:8080

Then try it from another terminal:

$ curl http://127.0.0.1:8080/
Hello, world!
$ curl http://127.0.0.1:8080/hello/zig
Hello, zig!

In a debug build, the server also logs each request it gets.

Here is what the code does:

  1. App holds the state of your application. Here it's only a greeting. In a real server it would be things like a database pool or settings loaded at startup.
  2. http.Server(App).init creates the server. It takes an allocator, the std.Io implementation, the configuration, and a pointer to the app.
  3. The configuration has a single listener on 127.0.0.1:8080, so the server can only be reached from your own machine. Use 0.0.0.0 to accept connections from other machines.
  4. server.router.get registers a handler for GET requests to a path. In /hello/:name, the :name part matches one segment of the path.
  5. server.run() accepts connections and serves them until the program is stopped.

A handler declares only the parameters it needs. The order doesn't matter. Both handlers take *App and *http.Response. The *App is the pointer you passed to init. hello also takes http.Param("name", []const u8), which holds the :name segment in .value. A handler that needs the request itself, for example to read its headers or body, can also take *http.Request.

res.print formats the body and sets the content type from its first argument. .text sends text/plain; charset=UTF-8. The response is sent after the handler returns.

Running in Production

The server above runs on init.io, which is std.Io.Threaded from the standard library. It gives every connection its own OS thread. That's fine for development. See Deployment for what to use in production.

Next Steps

  • Routing covers path parameters, wildcards and groups.
  • Handlers lists everything a handler can take and explains how errors become responses.
  • Configuration has the listener, timeout and limit settings.
  • Making Requests covers the HTTP client.