Skip to content

Part 1: Setup & First View

In this first part you'll create a Go module, set up a minimal Burrow server, and serve a "Hello, Polls!" homepage.

Source code: tutorial/step01/

Create the Project

mkdir polls && cd polls
go mod init polls
go get github.com/oliverandrich/burrow@latest

After writing the code below, run go mod tidy to fetch all transitive dependencies before building.

Write the Server

Create main.go:

package main

import (
    "context"
    "log"
    "net/http"
    "os"

    "github.com/go-chi/chi/v5"
    "github.com/oliverandrich/burrow"
    _ "github.com/oliverandrich/den/backend/sqlite" // register sqlite:// scheme
    "github.com/urfave/cli/v3"
)

func main() {
    srv := burrow.NewServer(
        &homepageApp{},
    )

    cmd := &cli.Command{
        Name:    "polls",
        Usage:   "Polls tutorial application",
        Version: "0.1.0",
        Flags:   srv.Flags(nil),
        Action:  srv.Run,
    }

    if err := cmd.Run(context.Background(), os.Args); err != nil {
        log.Fatal(err)
    }
}

The Homepage App

Add the following code to the same main.go file, below the main() function.

Every Burrow application is composed of apps. Each app implements the burrow.App interface — a Name() that returns a unique identifier. Our homepage app also implements HasRoutes to register an HTTP endpoint:

type homepageApp struct{}

func (a *homepageApp) Name() string { return "homepage" }
func (a *homepageApp) Routes(r chi.Router) {
    r.Get("/", burrow.Handle(func(w http.ResponseWriter, r *http.Request) error {
        return burrow.Text(w, http.StatusOK, "Hello, Polls!")
    }))
}

A few things to note:

  • The blank import _ "github.com/oliverandrich/den/backend/sqlite" registers the SQLite backend with Den so sqlite:// DSNs work. Every binary that opens a database must blank-import the backend that matches its DSN — Burrow doesn't pull either one in by default so production binaries only link the engine they actually use.
  • burrow.HandlerFunc has the signature func(w http.ResponseWriter, r *http.Request) error — just like the standard library, but with an error return. This lets you propagate errors instead of handling them in every handler.
  • burrow.Handle() wraps a HandlerFunc into a standard http.HandlerFunc. If the handler returns an *HTTPError, it sends the appropriate status code and message.
  • burrow.Text() is a helper that writes a plain text response.

Run It

go run .

You should see log output indicating the server has started. Burrow creates an app.db SQLite database in the working directory. Open http://localhost:8080 in your browser — you'll see "Hello, Polls!".

What Happens at Boot

When srv.Run is called, Burrow follows roughly this sequence:

  1. Parse configuration from CLI flags, environment variables, and TOML files
  2. Open the database via Den (SQLite with WAL mode, or PostgreSQL)
  3. Register documents from all HasDocuments apps
  4. Call Configure() on each Configurable app with the shared AppConfig
  5. Build the global template set from all HasTemplates apps
  6. Set up the Chi router with core middleware
  7. Apply middleware from all HasMiddleware apps
  8. Register routes from all HasRoutes apps
  9. Start the HTTP server with graceful shutdown

This is the short version. The full ordering — i18n bundle creation, storage opening, translation loading, the second PostConfigure pass, seeding, and background-process startup — is documented in Boot Sequence. You won't need that level of detail until later in the tutorial.

Built-in CLI Flags

Because Burrow uses urfave/cli, your server gets flags for free:

go run . --help
go run . --host 0.0.0.0 --port 3000

Common flags include --host, --port, --database-dsn, and --log-level.

Next

In Part 2, you'll add a database, define models for questions and choices, and write your first migration.