Files
todo-api/CHANGELOG.md
Rene Nochebuena 3fcba82448 feat(todo-api): add full-stack POC demonstrating micro-lib v0.9.0
Runnable REST API exercising every micro-lib tier in a containerless setup: N-layer architecture, SQLite persistence, header-based auth simulating Firebase output, and bit-mask RBAC enforcement.

What's included:
- cmd/todo-api: minimal main delegating to application.Run
- internal/application: full object graph wiring — launcher, sqlite, httpserver, httpmw stack, routes in BeforeStart
- internal/domain: User entity, ResourceTodos constant, PermReadTodo/PermWriteTodo bit positions
- internal/repository: TodoRepository, UserRepository, DBPermissionProvider (SQLite via modernc)
- internal/service: TodoService, UserService with interface-based dependencies
- internal/handler: TodoHandler, UserHandler using httputil adapters and valid for input validation
- internal/middleware: Auth (X-User-ID → rbac.Identity) and Require (bit-mask permission gate)
- logAdapter: bridges logz.Logger.With return type to httpmw.Logger interface
- SQLite schema: users, user_role (bitmask), todos; migrations run in BeforeStart
- Routes: POST /users (open), GET+POST /todos (RBAC), GET /users (RBAC)

Tested-via: todo-api POC integration
Reviewed-against: docs/adr/
2026-03-19 13:55:08 +00:00

6.4 KiB

Changelog

All notable changes to this module will be documented in this file.

The format is based on Keep a Changelog.

[0.9.0-poc] - 2026-03-18

Added

This entry describes what the POC demonstrates, not a versioned library API surface.

Application structure

  • N-layer architecture: cmd/todo-apiinternal/application (wiring only) → internal/handlerinternal/serviceinternal/repositoryinternal/domain; each layer depends on its inner neighbours via interfaces, with application as the only package that owns the full object graph
  • application.Run() — top-level entry point; wires all dependencies, registers routes inside lc.BeforeStart, and calls lc.Run() to start the launcher lifecycle
  • Local logAdapter struct in application — bridges logz.Logger (whose With returns logz.Logger) to httpmw.Logger (whose With must return httpmw.Logger), demonstrating the pattern for resolving return-type mismatches between micro-lib interfaces without modifying either library

Domain layer (internal/domain)

  • Todo struct — ID int64, Title string, Done bool, CreatedAt time.Time, JSON-tagged
  • User struct — ID string, Name string, Email string, CreatedAt time.Time, JSON-tagged
  • ResourceTodos = "todos" constant — the resource key used in the user_role table
  • PermReadTodo rbac.Permission = 0 and PermWriteTodo rbac.Permission = 1 — bit positions for the todos resource permission mask

Repository layer (internal/repository)

  • TodoRepository interface — FindAll(ctx) ([]domain.Todo, error), Create(ctx, domain.Todo) (domain.Todo, error)
  • UserRepository interface — FindAll, FindByID, Create, SetPermissions(ctx, userID, resource string, mask rbac.PermissionMask) error
  • NewTodoRepository(db sqlite.Client) TodoRepository — SQLite-backed implementation; FindAll returns an empty slice (never nil) when no rows exist; Create uses RETURNING to retrieve the generated ID and timestamp
  • NewUserRepository(db sqlite.Client) UserRepository — SQLite-backed implementation; FindByID returns a xerrors.ErrNotFound-wrapped error on sql.ErrNoRows; SetPermissions uses INSERT ... ON CONFLICT DO UPDATE for upsert
  • DBPermissionProvider struct implementing rbac.PermissionProvider — resolves the integer bitmask from user_role for a given (uid, resource) pair; returns mask 0 (no permissions) for a missing row rather than an error
  • NewPermissionProvider(db sqlite.Client) *DBPermissionProvider

Service layer (internal/service)

  • CreateTodoRequest struct — Title string with validate:"required,min=1,max=255" tag
  • TodoService interface — FindAll(ctx) ([]domain.Todo, error), Create(ctx, CreateTodoRequest) (domain.Todo, error)
  • NewTodoService(repo repository.TodoRepository) TodoService
  • CreateUserRequest struct — Name string, Email string, CanRead bool, CanWrite bool, with validation tags; CanRead/CanWrite seed the permission bitmask for ResourceTodos at creation time
  • UserService interface — FindAll(ctx) ([]domain.User, error), Create(ctx, CreateUserRequest) (domain.User, error)
  • NewUserService(repo repository.UserRepository, idGen func() string) UserServiceidGen is injected (e.g. uuid.NewString) to mint user IDs

Handler layer (internal/handler)

  • TodoHandlerNewTodoHandler(svc service.TodoService, v valid.Validator) *TodoHandler; FindAll delegates to httputil.HandleNoBody[[]domain.Todo]; Create delegates to httputil.Handle[CreateTodoRequest, domain.Todo]
  • UserHandlerNewUserHandler(svc service.UserService, v valid.Validator) *UserHandler; FindAll and Create follow the same httputil adapter pattern

Middleware layer (internal/middleware)

  • Auth(userRepo repository.UserRepository) func(http.Handler) http.Handler — reads X-User-ID header, looks up the user, and stores an rbac.Identity in context via rbac.SetInContext; returns 401 JSON on missing header or unknown user ID; the output contract is identical to what httpauth-firebase.EnrichmentMiddleware produces, so swapping in real Firebase auth requires no changes to handlers or services
  • Require(provider rbac.PermissionProvider, resource string, perms ...rbac.Permission) func(http.Handler) http.Handler — reads rbac.Identity from context, resolves the permission mask, and enforces each required bit; returns 403 JSON on missing identity or insufficient permissions; returns 500 JSON if the provider errors

Micro-lib stack composition demonstrated

  • launcher orchestrating sqlite and httpserver through OnInit → OnStart → OnStop
  • httpserver with RequestID → Recover → RequestLogger middleware via httpmw
  • sqlite (modernc pure-Go, no CGO) as the persistence layer with inline schema migrations run in lc.BeforeStart
  • rbac identity and permission types as the shared contract across middleware, repository, and domain
  • valid for request body validation in handlers
  • httputil handler adapters (Handle, HandleNoBody) for all route handlers
  • xerrors ErrNotFound for typed not-found errors in the repository layer
  • Route registration in lc.BeforeStart so migrations complete before any route is reachable and the port is not bound until routes are registered

Routes

  • POST /users — open; no auth required (user bootstrapping)
  • GET /todos — requires Auth middleware and PermReadTodo permission
  • POST /todos — requires Auth middleware and PermWriteTodo permission
  • GET /users — requires Auth middleware and PermReadTodo permission on the todos resource

Design Notes

  • Header-based auth (X-User-ID) deliberately mirrors the rbac.Identity output contract of httpauth-firebase, proving that the RBAC contract is provider-agnostic; the POC can be upgraded to real Firebase auth by replacing the Auth middleware with no changes to any other layer
  • SQLite with no Docker dependency keeps the POC self-contained: a single go run ./cmd/todo-api is sufficient to run the full stack, with the database created as todos.db in the working directory on first run
  • Routes are registered in lc.BeforeStart rather than at construction time, making the lifecycle ordering explicit and auditable in a single closure: migrations run after db.OnInit, routes register after migrations, and srv.OnStart binds the port last