Quick Start
Build your first REST API
This guide walks you through building a complete REST API with CRUD operations.
Prerequisites
- Go 1.21 or later
- Humus installed (
go get github.com/z5labs/humus)
Project Setup
mkdir todo-api
cd todo-api
go mod init todo-api
go get github.com/z5labs/humus
Configuration
Create config.yaml:
rest:
port: 8080
otel:
service:
name: todo-api
sdk:
disabled: true # Disable for this example
Define Your Model
Create main.go:
package main
import (
"context"
"fmt"
"net/http"
"sync"
"github.com/z5labs/humus/rest"
)
type Todo struct {
ID string `json:"id"`
Title string `json:"title"`
Completed bool `json:"completed"`
}
// In-memory store
type TodoStore struct {
mu sync.RWMutex
todos map[string]Todo
}
func NewTodoStore() *TodoStore {
return &TodoStore{
todos: make(map[string]Todo),
}
}
func (s *TodoStore) Create(todo Todo) {
s.mu.Lock()
defer s.mu.Unlock()
s.todos[todo.ID] = todo
}
func (s *TodoStore) Get(id string) (Todo, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
todo, ok := s.todos[id]
return todo, ok
}
func (s *TodoStore) List() []Todo {
s.mu.RLock()
defer s.mu.RUnlock()
todos := make([]Todo, 0, len(s.todos))
for _, todo := range s.todos {
todos = append(todos, todo)
}
return todos
}
func (s *TodoStore) Update(todo Todo) bool {
s.mu.Lock()
defer s.mu.Unlock()
if _, exists := s.todos[todo.ID]; !exists {
return false
}
s.todos[todo.ID] = todo
return true
}
func (s *TodoStore) Delete(id string) bool {
s.mu.Lock()
defer s.mu.Unlock()
if _, exists := s.todos[id]; !exists {
return false
}
delete(s.todos, id)
return true
}
Configuration Struct
type Config struct {
rest.Config `config:",squash"`
}
Main Function
func main() {
rest.Run(rest.YamlSource("config.yaml"), Init)
}
Initialize API
func Init(ctx context.Context, cfg Config) (*rest.Api, error) {
store := NewTodoStore()
// Create todo handler
createHandler := rest.HandlerFunc[Todo, Todo](func(ctx context.Context, req *Todo) (*Todo, error) {
if req.ID == "" {
req.ID = fmt.Sprintf("todo-%d", len(store.todos)+1)
}
store.Create(*req)
return req, nil
})
// List todos handler
listHandler := rest.ProducerFunc[[]Todo](func(ctx context.Context) (*[]Todo, error) {
todos := store.List()
return &todos, nil
})
// Get todo handler
getHandler := rest.ProducerFunc[Todo](func(ctx context.Context) (*Todo, error) {
id := rest.PathParamValue(ctx, "id")
todo, ok := store.Get(id)
if !ok {
return nil, fmt.Errorf("todo not found")
}
return &todo, nil
})
// Update todo handler
updateHandler := rest.HandlerFunc[Todo, Todo](func(ctx context.Context, req *Todo) (*Todo, error) {
id := rest.PathParamValue(ctx, "id")
req.ID = id
if !store.Update(*req) {
return nil, fmt.Errorf("todo not found")
}
return req, nil
})
// Delete todo handler
deleteHandler := rest.ConsumerFunc[struct{}](func(ctx context.Context, req *struct{}) error {
id := rest.PathParamValue(ctx, "id")
if !store.Delete(id) {
return fmt.Errorf("todo not found")
}
return nil
})
// Create API with all endpoints
api := rest.NewApi(
"Todo API",
"1.0.0",
rest.Handle(http.MethodPost, rest.BasePath("/todos"), rest.HandleJson(createHandler)),
rest.Handle(http.MethodGet, rest.BasePath("/todos"), rest.ProduceJson(listHandler)),
rest.Handle(http.MethodGet, rest.BasePath("/todos").Param("id"), rest.ProduceJson(getHandler)),
rest.Handle(http.MethodPut, rest.BasePath("/todos").Param("id"), rest.HandleJson(updateHandler)),
rest.Handle(http.MethodDelete, rest.BasePath("/todos").Param("id"), rest.ConsumeOnlyJson(deleteHandler)),
)
return api, nil
}
Complete Code
Put it all together in main.go:
package main
import (
"context"
"fmt"
"net/http"
"sync"
"github.com/z5labs/humus/rest"
)
type Todo struct {
ID string `json:"id"`
Title string `json:"title"`
Completed bool `json:"completed"`
}
type TodoStore struct {
mu sync.RWMutex
todos map[string]Todo
}
func NewTodoStore() *TodoStore {
return &TodoStore{
todos: make(map[string]Todo),
}
}
func (s *TodoStore) Create(todo Todo) {
s.mu.Lock()
defer s.mu.Unlock()
s.todos[todo.ID] = todo
}
func (s *TodoStore) Get(id string) (Todo, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
todo, ok := s.todos[id]
return todo, ok
}
func (s *TodoStore) List() []Todo {
s.mu.RLock()
defer s.mu.RUnlock()
todos := make([]Todo, 0, len(s.todos))
for _, todo := range s.todos {
todos = append(todos, todo)
}
return todos
}
func (s *TodoStore) Update(todo Todo) bool {
s.mu.Lock()
defer s.mu.Unlock()
if _, exists := s.todos[todo.ID]; !exists {
return false
}
s.todos[todo.ID] = todo
return true
}
func (s *TodoStore) Delete(id string) bool {
s.mu.Lock()
defer s.mu.Unlock()
if _, exists := s.todos[id]; !exists {
return false
}
delete(s.todos, id)
return true
}
type Config struct {
rest.Config `config:",squash"`
}
func main() {
rest.Run(rest.YamlSource("config.yaml"), Init)
}
func Init(ctx context.Context, cfg Config) (*rest.Api, error) {
store := NewTodoStore()
// Create todo handler
createHandler := rest.HandlerFunc[Todo, Todo](func(ctx context.Context, req *Todo) (*Todo, error) {
if req.ID == "" {
req.ID = fmt.Sprintf("todo-%d", len(store.todos)+1)
}
store.Create(*req)
return req, nil
})
// List todos handler
listHandler := rest.ProducerFunc[[]Todo](func(ctx context.Context) (*[]Todo, error) {
todos := store.List()
return &todos, nil
})
// Get todo handler
getHandler := rest.ProducerFunc[Todo](func(ctx context.Context) (*Todo, error) {
id := rest.PathParamValue(ctx, "id")
todo, ok := store.Get(id)
if !ok {
return nil, fmt.Errorf("todo not found")
}
return &todo, nil
})
// Update todo handler
updateHandler := rest.HandlerFunc[Todo, Todo](func(ctx context.Context, req *Todo) (*Todo, error) {
id := rest.PathParamValue(ctx, "id")
req.ID = id
if !store.Update(*req) {
return nil, fmt.Errorf("todo not found")
}
return req, nil
})
// Delete todo handler
deleteHandler := rest.ConsumerFunc[struct{}](func(ctx context.Context, req *struct{}) error {
id := rest.PathParamValue(ctx, "id")
if !store.Delete(id) {
return fmt.Errorf("todo not found")
}
return nil
})
// Create API with all endpoints
api := rest.NewApi(
"Todo API",
"1.0.0",
rest.Handle(http.MethodPost, rest.BasePath("/todos"), rest.HandleJson(createHandler)),
rest.Handle(http.MethodGet, rest.BasePath("/todos"), rest.ProduceJson(listHandler)),
rest.Handle(http.MethodGet, rest.BasePath("/todos").Param("id"), rest.ProduceJson(getHandler)),
rest.Handle(http.MethodPut, rest.BasePath("/todos").Param("id"), rest.HandleJson(updateHandler)),
rest.Handle(http.MethodDelete, rest.BasePath("/todos").Param("id"), rest.ConsumeOnlyJson(deleteHandler)),
)
return api, nil
}
Run the Service
go run main.go
Test the API
# Create a todo
curl -X POST http://localhost:8080/todos \
-H "Content-Type: application/json" \
-d '{"title": "Learn Humus", "completed": false}'
# List all todos
curl http://localhost:8080/todos
# Get a specific todo
curl http://localhost:8080/todos/todo-1
# Update a todo
curl -X PUT http://localhost:8080/todos/todo-1 \
-H "Content-Type: application/json" \
-d '{"title": "Learn Humus", "completed": true}'
# Delete a todo
curl -X DELETE http://localhost:8080/todos/todo-1
# View OpenAPI spec
curl http://localhost:8080/openapi.json
What’s Happening
- rest.Run() loads config and calls Init
- rest.NewApi() creates the API with name and version
- rest.HandleJson/ProduceJson/ConsumeOnlyJson wrap handlers with type-safe serialization
- rest.Handle() registers handlers at specific paths as API options
- Automatic instrumentation traces all requests
- OpenAPI generation creates
/openapi.jsonfrom your types
Securing Your API (Optional)
Add JWT authentication to protect write operations:
1. Create a Simple JWT Verifier
import (
"context"
"fmt"
)
type SimpleJWTVerifier struct{}
func (v *SimpleJWTVerifier) Verify(ctx context.Context, token string) (context.Context, error) {
// In production, verify the JWT signature and claims
// For this example, we just accept any non-empty token
if token == "" {
return nil, fmt.Errorf("empty token")
}
// Extract user info (in production, parse from JWT claims)
userID := "user-from-token"
return context.WithValue(ctx, "user_id", userID), nil
}
2. Protect Create/Update/Delete Operations
func registerHandlers(api *rest.Api, store *TodoStore) {
verifier := &SimpleJWTVerifier{}
// Public endpoint - no auth required
listHandler := rest.ProducerFunc[[]Todo](func(ctx context.Context) (*[]Todo, error) {
todos := store.List()
return &todos, nil
})
rest.Handle(http.MethodGet, rest.BasePath("/todos"), rest.ProduceJson(listHandler))
// Protected endpoint - JWT required
createHandler := rest.HandlerFunc[Todo, Todo](func(ctx context.Context, req *Todo) (*Todo, error) {
if req.ID == "" {
req.ID = fmt.Sprintf("todo-%d", len(store.todos)+1)
}
store.Create(*req)
return req, nil
})
rest.Handle(
http.MethodPost,
rest.BasePath("/todos"),
rest.HandleJson(createHandler),
rest.Header("Authorization", rest.Required(), rest.JWTAuth("jwt", verifier)),
)
// Other endpoints...
}
3. Test with Authentication
# Fails - no Authorization header
curl -X POST http://localhost:8080/todos \
-H "Content-Type: application/json" \
-d '{"title": "Protected todo"}'
# Returns: 401 Unauthorized
# Success - with Bearer token
curl -X POST http://localhost:8080/todos \
-H "Content-Type: application/json" \
-H "Authorization: Bearer my-token" \
-d '{"title": "Protected todo"}'
# Returns: 200 OK
For production JWT implementation with proper signature verification, see Authentication.
Next Steps
- Learn about Authentication for production-ready JWT verification
- Read Handler Helpers for type-safe handlers and serialization
- See Routing for path parameters and validation
- Understand Error Handling for custom error responses