Aiming for Jarvis, Creating D.A.N.I.

Tutorial 17: Building a web server (or, Why I decided to stop fighting the network)

If you've been following these tutorials, you've got a handle on the language, the file system, and even the madness of concurrency. Now, we're going to drag our code onto the web. It's the logical next step, though I'll be the first to admit that exposing your code to the internet is a bit like letting a toddler loose in a china shop—things are going to get interesting, and possibly very loud.

The Router: Keeping things in order

We keep our web logic clean by using a router. It takes the incoming noise of HTTP requests and points them where they need to go. A Handler is essentially just a function that knows how to talk to a ResponseWriter.

module main

import "std/web/router"
import "std/web/server"
import "std/web/types"

fn home(req: types.Request, w: *types.ResponseWriter) {
    w.Write("Hello, Skink!")
}

fn greet(req: types.Request, w: *types.ResponseWriter) {
    // Routing params let us capture bits of the path
    name := req.URLParam("name")
    w.Write("Hello, {name}!")
}

fn jsonDemo(req: types.Request, w: *types.ResponseWriter) {
    // JSON is the universal tongue of the web
    w.JSON(200, "{\"ok\":true}")
}

Starting the engine

The server doesn't just run; you have to point it at a port and a router.

fn main() -> int {
    r := router.NewRouter()
    r.Get("/", home)
    r.Get("/hello/:name", greet)
    r.Get("/api/status", jsonDemo)

    s := server.NewServer(8080, &r)
    err := s.ListenAndServe()
    if err.message != "" {
        print("server error: {err.message}")
        return 1
    }
    return 0
}

The Request: Your window into the madness

The Request object is your primary interface to the outside world. It tells you exactly what the user is throwing at you—headers, query parameters, and, if they're particularly pushy, the body content.

fn search(req: types.Request, w: *types.ResponseWriter) {
    q := req.QueryParam("q")
    w.Write("You searched for: {q}")
}

Middleware: The gatekeeper

If you need to log requests, authorize users, or just add some headers, don't mess up your business logic. Use middleware. It wraps your handlers, letting you execute code before or after the "real work" happens.

fn logging(next: types.Handler) -> types.Handler {
    return fn(req: types.Request, w: *types.ResponseWriter) {
        print("{req.Method()} {req.Path()}")
        next(req, w)
    }
}

Exercises

Don't just launch a server and wait for the internet to start screaming at you; test it properly:

  1. The Echo Chamber: Write a POST /echo handler that reads req.Body() and sends the exact same text back. It's the "Hello, World" of networking.
  2. The Dynamic User: Implement a /user/:id route that returns a JSON object containing that ID.
  3. The Local Test: Use curl to hit your server. If you don't have curl, well... you should probably get it. It’s the Swiss Army knife of the web.

Next

When you're ready to start storing real, persistent data rather than just JSON files, we'll talk about Working with SQLite.