Getting started

From an empty folder to a humus service that runs with go run and passes a test that runs the whole App with humustest.Start, with nothing but a Go toolchain.

The service is a job: it greets the name its environment gives it, once, and exits. It needs Go 1.27 or later, and nothing else. Its code is example/job/hello.

1. Create the module

mkdir hello && cd hello
go mod init example.com/hello
go get github.com/z5labs/humus@v0.16.0

2. Write Init

Init builds the service. It reads HELLO_NAME from the environment and returns a job, whose run greets it. Put it in hello.go:

package main

import (
	"context"
	"fmt"

	"github.com/z5labs/bedrock/config"

	"github.com/z5labs/humus"
	"github.com/z5labs/humus/job"
)

func Init(ctx context.Context, s *humus.Setup) ([]humus.Service, error) {
	name, err := config.Env(s.Process.Environment, "HELLO_NAME").Read(ctx)
	if err != nil {
		return nil, err
	}

	out := s.Process.Stdout
	greet := func(context.Context) error {
		_, err := fmt.Fprintf(out, "Hello, %s!\n", name)
		return err
	}
	return []humus.Service{job.NewService(s, greet)}, nil
}

3. Write main

humus.Main runs Init’s Services until they return, then exits with their status. Put it in main.go:

// Command hello greets the name its environment gives it, once.
package main

import "github.com/z5labs/humus"

func main() {
	humus.Main(Init)
}

4. Run it

go mod tidy
HELLO_NAME=Gopher go run .

The greeting comes between the App’s own log records:

{"timestamp":"2026-10-23T06:04:36.982513033Z","severity_text":"INFO","severity_number":9,"body":"serving","scope":"github.com/z5labs/humus"}
Hello, Gopher!
{"timestamp":"2026-10-23T06:04:36.982631066Z","severity_text":"INFO","severity_number":9,"body":"shutting down","scope":"github.com/z5labs/humus"}
{"timestamp":"2026-10-23T06:04:36.982656447Z","severity_text":"INFO","severity_number":9,"body":"stopped","scope":"github.com/z5labs/humus"}

Without HELLO_NAME, Init fails, and the service exits 78:

go run .
{"timestamp":"2026-10-23T06:04:37.083766967Z","severity_text":"ERROR","severity_number":17,"body":"init: config HELLO_NAME: not found","scope":"github.com/z5labs/humus"}
exit status 78

5. Test it

humustest.Start runs Init in the App humus.Main would run, on a test process whose environment the test sets and whose output it reads. Put the tests in hello_test.go:

package main

import (
	"fmt"
	"strings"
	"testing"
	"testing/synctest"

	"github.com/z5labs/humus/humustest"
)

func TestInit_greets_the_name_it_is_given(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
		p, status := humustest.Start(t, Init,
			humustest.WithEnv(map[string]string{"HELLO_NAME": "Gopher"}),
		)

		st := <-status
		if st.Code() != 0 {
			t.Fatalf("code = %d, want 0: %v", st.Code(), st.Cause())
		}
		if !strings.Contains(p.Stdout(), "Hello, Gopher!\n") {
			t.Errorf("stdout = %q, want it to greet Gopher", p.Stdout())
		}
	})
}

func TestInit_fails_without_a_name(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
		_, status := humustest.Start(t, Init)

		st := <-status
		if st.Code() != 78 {
			t.Errorf("code = %d, want 78: %v", st.Code(), st.Cause())
		}
		if !strings.Contains(fmt.Sprint(st.Cause()), "HELLO_NAME") {
			t.Errorf("cause = %v, want it to name HELLO_NAME", st.Cause())
		}
	})
}
go test ./...

Next

Fundamentals covers what this service already has, as every humus service does: the App lifecycle, configuration, telemetry, health and testing. Then Service types has a section for each kind of service humus runs: HTTP, gRPC, jobs, Kafka, RabbitMQ and Kubernetes controllers.