A small Go web service that demonstrates a favourites API:
- list a user's favourites
- add/remove an asset
- update an asset's description.
The project uses a concurrency-safe in-memory store and a simple JWT-based auth middleware for protected routes.
This README provides a concise developer guide:
- How to build, run and test locally
- API endpoints and example curl commands
- Notes on the in-memory store and where to add persistence
From the repository root:
go build ./...Start the server (binds to :3002):
go run ./cmd/appThe server seeds an in-memory store on startup and prints two seeded user UUIDs you can use in examples.
Run the unit tests:
go test ./...Current tests cover the handler layer and in-memory store.
A Dockerfile is provided that builds the Go binary inside a golang builder image and produces a runnable image that exposes port 3002. The current Dockerfile uses a single-stage build based on the official golang image and outputs a static binary named favourites-app.
Build the image:
docker build -t platform-go-challenge:latest .Run the container (maps local 3002 to container 3002):
docker run --rm -p 3002:3002 platform-go-challenge:latestAll protected endpoints require a Bearer JWT token. Use the token generation endpoint to get a token for a given user UUID. Note: the auth middleware requires the token's user (subject) to match the {userId} path parameter — otherwise requests will be rejected with 403 Forbidden.
- Generate token
- POST /token
- Body: { "user_id": "" }
- Response: { "token": "" }
Example:
curl -s -X POST -H "Content-Type: application/json" \
-d '{"user_id":"431ff79b-4e65-4625-9f7f-c8125e2be057"}' \
http://localhost:3002/tokenUse the returned token in the Authorization: Bearer <token> header for subsequent requests. The token subject must equal the {userId} path parameter on protected routes.
- Add favourite
- POST /users/{userId}/favourites
- Header: Authorization: Bearer
- Body: JSON representation of
model.Asset. For convenience the tests and seed helpers usemodel.NewAsset(...)shapes. Minimal example:
{
"type": "insight",
"description": "Top insight",
"data": { "text": "40% of..." }
}Example:
curl -X POST -H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{"type":"insight","description":"Test desc","data":{"text":"hello"}}' \
http://localhost:3002/users/<user-uuid>/favouritesSuccessful response: 201 Created with JSON { "message": "favourite added", "favourite": { ... } }
- List favourites
- GET /users/{userId}/favourites
- Header: Authorization: Bearer
- Query params (optional): offset, limit, sortBy (description|type), order (asc|desc)
Example:
curl -H "Authorization: Bearer <token>" \
http://localhost:3002/users/<user-uuid>/favouritesResponse: 200 OK { "favourites": [ ... ] }
- Patch description
- PATCH /users/{userId}/favourites/{assetId}
- Header: Authorization: Bearer
- Body: { "description": "New description" }
- Response: 204 No Content on success (current implementation sends a JSON message with the 204 status)
- Delete favourite
- DELETE /users/{userId}/favourites/{assetId}
- Header: Authorization: Bearer
- Response: 200 OK { "message": "favourite removed", "favouriteId": "" }
The seed helper pre-populates two demo users. When you run the server you should see printed UUIDs similar to:
- 431ff79b-4e65-4625-9f7f-c8125e2be057
- d61733f6-910e-4e26-bb31-0cac3a7d4279
Use these UUIDs to generate tokens and try the endpoints.
The storage abstraction is defined by internal/db.FavouriteRepository. To add persistence (Postgres, Redis, etc.) implement that interface and replace the db.NewMemoryStore() call in cmd/app/main.go with your implementation.
Key methods to implement:
- GetAllFavourites(userID uuid.UUID, q PageQuery) ([]model.Asset, error)
- AddFavourite(userID uuid.UUID, a model.Asset) error
- RemoveFavourite(userID uuid.UUID, assetID uuid.UUID) error
- PatchFavourite(userID uuid.UUID, assetID uuid.UUID, description string) error
- The store is in-memory and ephemeral. Restarting the server clears all runtime state.
- The JWT secret is hard-coded for the challenge and should be replaced by a secure secret management strategy for any real deployment.
- The
model.Asset.Datafield usesanyto keep the example compact. For a production service a well-defined schema per asset type with validation is recommended.
The project uses swaggo/swag to generate Swagger documentation. To regenerate the docs run the following command from the repository root:
$(go env GOPATH)/bin/swag init -g main.go -d cmd/app,internal/api,internal/model --parseInternal -o internal/swaggerNotes:
- The command writes generated files into the
internal/swaggerdirectory (adjust-oif you prefer a different output location). - Running
swag initwill overwrite the generated files; if you manually customizeinternal/swaggeryou may want to keep a backup or script changes post-generation.