Files
todo-api/CLAUDE.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

todo-api

POC application demonstrating the full micro-lib stack: N-layer architecture, SQLite persistence, header-based auth, and RBAC permission enforcement.

Purpose

This is not a library. It is a runnable REST API that exercises every micro-lib tier in a realistic but containerless setup. The goal is to validate that the micro-lib modules compose correctly, that the RBAC contract between httpmw.Auth-equivalent middleware and rbac.PermissionMask works end-to-end, and that the launcher lifecycle manages database and HTTP server startup cleanly.

Tier & Dependencies

Tier 5 (application). Imports:

  • code.nochebuena.dev/go/launcher — lifecycle orchestration
  • code.nochebuena.dev/go/httpserver — HTTP server (Tier 4)
  • code.nochebuena.dev/go/httpmw — request ID, recovery, request logger middleware
  • code.nochebuena.dev/go/logz — structured logger
  • code.nochebuena.dev/go/sqlite — SQLite client (Tier 1)
  • code.nochebuena.dev/go/rbac — identity and permission types
  • code.nochebuena.dev/go/valid — input validation
  • code.nochebuena.dev/go/httputil — handler adapters
  • code.nochebuena.dev/go/xerrors — typed errors (NotFound)
  • github.com/go-chi/chi/v5 — used in application for route grouping
  • github.com/google/uuid — user ID generation

Key Design Decisions

  • N-layer architecture (ADR-001): cmd → application → handler → service → repository → domain. Each layer depends only on its inner neighbours via interfaces. application is the only package that knows the full object graph.
  • Header-based auth (ADR-002): middleware.Auth reads X-User-ID, looks up the user, and puts an rbac.Identity in context — identical output contract to httpauth-firebase. Swapping in real Firebase auth requires only replacing the middleware.
  • Domain-owned permission bits (ADR-003): domain.PermReadTodo = 0 and domain.PermWriteTodo = 1 define bit positions. domain.ResourceTodos = "todos" is the key in user_role. All layers import these constants from domain.
  • Routes registered in BeforeStart: Route registration happens in lc.BeforeStart(...) so SQLite tables exist before handlers are reachable. This is the correct place in the launcher lifecycle — after db.OnInit migrates the schema, before srv.OnStart binds the port.
  • logAdapter: logz.Logger.With(...) returns logz.Logger; httpmw.Logger.With(...) must return httpmw.Logger. A local logAdapter struct in application bridges this mismatch without requiring either library to change.
  • SQLite — no container required: The database file is todos.db in the working directory. Local development and testing require no Docker, no Postgres, no network access.

Patterns

Layer interfaces (defined at consumption site in Go convention):

repository.TodoRepository — consumed by service
repository.UserRepository — consumed by service and middleware
rbac.PermissionProvider  — consumed by middleware.Require
service.TodoService      — consumed by handler
service.UserService      — consumed by handler

Full middleware + route wiring in BeforeStart:

lc.BeforeStart(func() error {
    // run migrations
    db.GetExecutor(ctx).ExecContext(ctx, migrate)

    // open endpoint
    srv.Post("/users", userH.Create)

    // auth-gated group
    srv.Group(func(r chi.Router) {
        r.Use(appMW.Auth(userRepo))
        r.With(appMW.Require(permProvider, domain.ResourceTodos, domain.PermReadTodo)).
            Get("/todos", todoH.FindAll)
        r.With(appMW.Require(permProvider, domain.ResourceTodos, domain.PermWriteTodo)).
            Post("/todos", todoH.Create)
    })
    return nil
})

Creating a user with permissions (curl):

# Create a user with read+write on todos
curl -X POST http://localhost:3000/users \
  -H 'Content-Type: application/json' \
  -d '{"name":"Alice","email":"alice@example.com","can_read":true,"can_write":true}'

# Use the returned ID to authenticate
curl http://localhost:3000/todos -H 'X-User-ID: <id>'

Database schema:

CREATE TABLE users (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    email TEXT NOT NULL UNIQUE,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE user_role (
    user_id     TEXT    NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    resource    TEXT    NOT NULL,
    permissions INTEGER NOT NULL DEFAULT 0,
    PRIMARY KEY (user_id, resource)
);

CREATE TABLE todos (
    id         INTEGER  PRIMARY KEY AUTOINCREMENT,
    title      TEXT     NOT NULL,
    done       BOOLEAN  NOT NULL DEFAULT 0,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);

What to Avoid

  • Do not add business logic to application. It is a wiring package only.
  • Do not import application from any internal/ sub-package. The dependency arrow points into application, never out.
  • Do not define permission constants in repository or middleware. They belong in domain (ADR-003).
  • Do not use the header auth pattern in production. X-User-ID is unauthenticated. Replace with httpauth-firebase or equivalent before any real deployment.
  • Do not reuse or reassign existing permission bit positions. Append new bits at the next available position.
  • Do not run migrations outside of BeforeStart. Running them in OnInit would execute before other components are ready; running them in OnStart races with route availability.
  • Do not add telemetry imports to this POC unless demonstrating the telemetry module specifically. The OTel no-op default applies — adding telemetry.New is a one-line addition if needed.

Testing Notes

  • There are no tests in this POC currently. The structure is designed for testability: each layer depends only on interfaces, so handlers, services, and repositories can be tested with mock implementations.
  • Service-layer tests: implement repository.TodoRepository / repository.UserRepository as in-memory stubs, construct the service, and call methods directly.
  • Handler-layer tests: implement service.TodoService as a stub, use httptest.NewRequest + httptest.NewRecorder with the handler's ServeHTTP.
  • Middleware tests: use httptest.NewRequest, set X-User-ID, and verify that rbac.Identity is present in context after Auth runs.
  • Integration tests (optional): start a real SQLite in-memory database (sqlite.Config{Path: ":memory:"}), run migrations, and exercise the full stack.