REST API for a task management system. Register, log in, create projects, add tasks, assign them — the usual workflow but built properly with auth, ownership checks, and clean error handling.
Stack: Go 1.22 · PostgreSQL 16 · chi router · pgx · JWT (golang-jwt) · bcrypt · Docker
backend/
├── cmd/server/ # Entrypoint
├── internal/
│ ├── config/ # Env-based config
│ ├── database/ # Postgres connection (retry on startup)
│ ├── handler/ # HTTP handlers
│ ├── middleware/ # JWT auth
│ ├── model/ # Domain types
│ ├── repository/ # SQL queries (pgx, no ORM)
│ ├── router/ # Routes (chi)
│ └── service/ # Business logic
├── migrations/ # Up + down SQL migrations
├── seed/ # Seed data
├── tests/ # Integration tests
├── Dockerfile # Multi-stage build
└── postman_collection.json
The code follows a straightforward layered pattern: Handler → Service → Repository → DB.
- Handlers deal with HTTP: parsing requests, writing responses, returning the right status codes.
- Services hold the business rules — things like "only the project owner can delete" or "default status is todo."
- Repositories own all the SQL. Nothing else touches the database.
Why chi? It's a thin router that plays nicely with net/http. Middleware chaining, URL params, route groups — it has what I need without pulling in a framework.
Why pgx over an ORM? I needed dynamic WHERE/SET clauses for task filtering and partial updates. Writing SQL directly felt cleaner than fighting an ORM for these cases.
Why creator_id on tasks? The spec says only the task creator or project owner can delete a task, so I needed to track who created it. The spec allows adding fields.
What I chose not to do:
- No repository interfaces — concrete types are easier to follow at this scale. I'd add interfaces if the project grew or needed mocked tests.
- No request ID in logs — chi generates one, but I didn't thread it into slog. Would fix in a real service.
- Integration tests live in one file. I'd split by domain in a bigger project.
git clone https://github.com/cybertronprime/zomato_task_app
cd zomato_task_app
cp .env.example .env
docker compose upAPI is at http://localhost:8080. Migrations and seed data run automatically on startup.
Handled automatically by the entrypoint script — no manual steps.
If you ever need to run them by hand:
brew install golang-migrate
# up
migrate -path backend/migrations \
-database "postgres://taskflow:taskflow_secret@localhost:5432/taskflow?sslmode=disable" up
# down
migrate -path backend/migrations \
-database "postgres://taskflow:taskflow_secret@localhost:5432/taskflow?sslmode=disable" downSeeded on startup:
Email: test@example.com
Password: password123
| Method | Endpoint | Description |
|---|---|---|
| POST | /auth/register | Register (name, email, password) |
| POST | /auth/login | Returns JWT |
// POST /auth/register
{ "name": "Jane Doe", "email": "jane@example.com", "password": "secret123" }
// → 201 { "token": "...", "user": { "id": "...", "name": "Jane Doe", "email": "jane@example.com" } }
// POST /auth/login
{ "email": "jane@example.com", "password": "secret123" }
// → 200 { "token": "...", "user": { ... } }| Method | Endpoint | Description |
|---|---|---|
| GET | /projects | List projects (paginated: ?page=&limit=) |
| POST | /projects | Create project |
| GET | /projects/:id | Project + its tasks |
| PATCH | /projects/:id | Update name/description (owner only) |
| DELETE | /projects/:id | Delete project + tasks (owner only) |
| GET | /projects/:id/stats | Task counts by status and assignee |
| Method | Endpoint | Description |
|---|---|---|
| GET | /projects/:id/tasks | List tasks (?status=&assignee=&page=&limit=) |
| POST | /projects/:id/tasks | Create task |
| PATCH | /tasks/:id | Update fields (all optional) |
| DELETE | /tasks/:id | Delete (project owner or task creator) |
// 400
{ "error": "validation failed", "fields": { "email": "is required" } }
// 401
{ "error": "unauthorized" }
// 403
{ "error": "forbidden" }
// 404
{ "error": "not found" }A Postman collection is included at backend/postman_collection.json.
docker compose run --rm testThis spins up a container with the Go toolchain, creates a test database, runs migrations, and executes the integration tests. No local Go install needed.
- More tests. The integration tests cover happy paths but I'd want unit tests per service with mocked repos, plus edge cases like concurrent updates.
- Transactions. Deleting a project relies on CASCADE. I'd wrap multi-step operations in explicit transactions for safety.
- Validation library. The manual field checking in handlers is repetitive — something like
go-playground/validatorwould clean it up. - Rate limiting on auth endpoints to prevent brute-force login attempts.
- CI pipeline. GitHub Actions for lint + test + build on every push.