6.8 KiB
6.8 KiB
Feature: User Preferences API
Problem Statement
Users need the ability to store and retrieve personal preferences (theme, language, notification settings) so that their experience is personalized and consistent across sessions. Currently, the preferences-api service has only scaffolded example endpoints with no real preference management functionality.
User Stories
- As an application user, I want to retrieve my preferences so that the UI reflects my chosen theme, language, and notification settings.
- As an application user, I want to update my preferences so that changes persist across sessions.
- As a frontend application, I want to fetch preferences by user ID on login so that I can render the correct theme and locale immediately.
- As a system administrator, I want preferences stored as structured key-value pairs so that new preference types can be added without schema changes.
Acceptance Criteria
GET /api/preferences-api/preferences/{user_id}returns all preferences for a user as key-value pairsGET /api/preferences-api/preferences/{user_id}returns200with empty preferences object when user has no stored preferencesGET /api/preferences-api/preferences/{user_id}returns404only if user_id format is invalid (not a valid UUID)PUT /api/preferences-api/preferences/{user_id}creates or updates preferences (upsert behavior)PUT /api/preferences-api/preferences/{user_id}accepts a JSON body with apreferencesobject containing key-value pairsPUT /api/preferences-api/preferences/{user_id}returns200with the updated preferencesPUT /api/preferences-api/preferences/{user_id}validates known preference keys against allowed values:theme:"light","dark","system"language: valid BCP-47 language tag (e.g.,"en","fr","es","de","ja")notifications_enabled:trueorfalse
PUT /api/preferences-api/preferences/{user_id}returns400with details when validation fails- Unknown preference keys are accepted and stored (extensibility for future preference types)
- Preferences are persisted in PostgreSQL and survive service restarts
- All responses follow the
{data, meta}envelope pattern - OpenAPI spec documents both endpoints with request/response schemas
- Handler tests cover success paths and error cases
- Service-layer tests cover business logic (validation, upsert behavior)
- Database migration creates the preferences table
API Design
GET /api/preferences-api/preferences/{user_id}
Response 200:
{
"data": {
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"preferences": {
"theme": "dark",
"language": "en",
"notifications_enabled": true
},
"updated_at": "2026-02-09T12:00:00Z"
},
"meta": {
"request_id": "...",
"timestamp": "..."
}
}
PUT /api/preferences-api/preferences/{user_id}
Request Body:
{
"preferences": {
"theme": "dark",
"language": "fr",
"notifications_enabled": false
}
}
Response 200:
{
"data": {
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"preferences": {
"theme": "dark",
"language": "fr",
"notifications_enabled": false
},
"updated_at": "2026-02-09T12:01:00Z"
},
"meta": {
"request_id": "...",
"timestamp": "..."
}
}
Response 400 (validation error):
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid preference values",
"details": {
"theme": "must be one of: light, dark, system"
}
},
"meta": {
"request_id": "...",
"timestamp": "..."
}
}
Technical Constraints
- Must follow existing hexagonal architecture: domain -> service -> port -> adapter
- Must use
app.Wrap()handler pattern,httpresponse.*envelope,httperror.*error types - Must use
{param}brace syntax for chi URL parameters (never colon syntax) - Database adapter uses
pkg/database(sqlx) with embedded SQL migrations - Preferences stored as JSONB column in PostgreSQL for flexible key-value storage
user_idis a UUID path parameter (validated at handler level)- PUT is idempotent: creates preferences row if none exists, updates if it does (upsert via
ON CONFLICT) - Service runs on port 8001 under route prefix
/api/preferences-api
Database Schema
-- migrations/001_create_preferences.sql
CREATE TABLE IF NOT EXISTS preferences (
user_id UUID PRIMARY KEY,
preferences JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_preferences_updated_at ON preferences (updated_at);
Hexagonal Architecture Mapping
| Layer | File | Responsibility |
|---|---|---|
| Domain | internal/domain/preference.go |
UserPreferences model, validation rules for known keys |
| Port | internal/port/preference.go |
PreferenceRepository interface (Get, Upsert) |
| Service | internal/service/preference.go |
PreferenceService with validation + delegation to port |
| Adapter | internal/adapter/postgres/preference.go |
PostgreSQL implementation of PreferenceRepository |
| Handler | internal/api/handlers/preference.go |
HTTP handlers for GET/PUT endpoints |
| Routes | internal/api/routes.go |
Register preference routes |
| Spec | internal/api/spec.go |
OpenAPI documentation for preference endpoints |
Dependencies
- PostgreSQL database (already configured via
DATABASE_URLin.env.example) pkg/databasepackage for connection pooling and migrationspkg/app,pkg/httperror,pkg/httpresponsefor handler patternspkg/openapifor API documentation- Existing preferences-api service scaffolding
Out of Scope
- Authentication/authorization enforcement (auth is opt-in per CLAUDE.md; can be layered on later)
- Per-preference-key endpoints (e.g.,
GET /preferences/{user_id}/theme) - full object only - Preference defaults management (hardcoded defaults in frontend, not API)
- Preference change history/audit log
- Bulk preference operations across multiple users
- Real-time preference change notifications (WebSocket/SSE)
- DELETE endpoint (preferences are upserted, not deleted)
Open Questions
- Should unknown preference keys have value-type validation? Currently spec allows any JSON value for unknown keys. Should we restrict to strings/booleans/numbers only?
- Should there be a maximum number of preference keys per user? A limit would prevent abuse but adds complexity.
- Should the API enforce that
user_idcorresponds to a real user? This requires a call to an auth/user service. Current spec treatsuser_idas an opaque UUID with no cross-service validation. - Should preference values have a max size limit? JSONB columns can hold large values; a per-value or total-size limit may be prudent.