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/
6.4 KiB
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 orchestrationcode.nochebuena.dev/go/httpserver— HTTP server (Tier 4)code.nochebuena.dev/go/httpmw— request ID, recovery, request logger middlewarecode.nochebuena.dev/go/logz— structured loggercode.nochebuena.dev/go/sqlite— SQLite client (Tier 1)code.nochebuena.dev/go/rbac— identity and permission typescode.nochebuena.dev/go/valid— input validationcode.nochebuena.dev/go/httputil— handler adapterscode.nochebuena.dev/go/xerrors— typed errors (NotFound)github.com/go-chi/chi/v5— used in application for route groupinggithub.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.applicationis the only package that knows the full object graph. - Header-based auth (ADR-002):
middleware.AuthreadsX-User-ID, looks up the user, and puts anrbac.Identityin context — identical output contract tohttpauth-firebase. Swapping in real Firebase auth requires only replacing the middleware. - Domain-owned permission bits (ADR-003):
domain.PermReadTodo = 0anddomain.PermWriteTodo = 1define bit positions.domain.ResourceTodos = "todos"is the key inuser_role. All layers import these constants fromdomain. - 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 — afterdb.OnInitmigrates the schema, beforesrv.OnStartbinds the port. - logAdapter:
logz.Logger.With(...)returnslogz.Logger;httpmw.Logger.With(...)must returnhttpmw.Logger. A locallogAdapterstruct inapplicationbridges this mismatch without requiring either library to change. - SQLite — no container required: The database file is
todos.dbin 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
applicationfrom anyinternal/sub-package. The dependency arrow points intoapplication, never out. - Do not define permission constants in
repositoryormiddleware. They belong indomain(ADR-003). - Do not use the header auth pattern in production.
X-User-IDis unauthenticated. Replace withhttpauth-firebaseor 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 inOnInitwould execute before other components are ready; running them inOnStartraces 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.Newis 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.UserRepositoryas in-memory stubs, construct the service, and call methods directly. - Handler-layer tests: implement
service.TodoServiceas a stub, usehttptest.NewRequest+httptest.NewRecorderwith the handler'sServeHTTP. - Middleware tests: use
httptest.NewRequest, setX-User-ID, and verify thatrbac.Identityis present in context afterAuthruns. - Integration tests (optional): start a real SQLite in-memory database (
sqlite.Config{Path: ":memory:"}), run migrations, and exercise the full stack.