REST API for personal expense management with JWT authentication.
Project based on roadmap.sh/projects/expense-tracker
# Clone and install
git clone <repo-url>
cd expense-api
# Configure environment variables
cp .env.example .env
# Run
go run cmd/api/main.goPORT=:8080
GIN_MODE=debug
DATABASE_URL=postgres://user:pass@localhost:5432/expenses
JWT_SECRET=your-secure-secretThe API uses JWT with two tokens:
| Token | Location | Duration | Purpose |
|---|---|---|---|
| Access Token | Header Authorization: Bearer <token> |
1 hour | Access protected endpoints |
| Refresh Token | HttpOnly Cookie refresh_token |
24 hours | Get new access token |
1. POST /users β Create account (returns access_token + sets refresh_token cookie)
2. POST /auth/login β Login (returns access_token + sets refresh_token cookie)
3. POST /auth/refresh β Renew access token (uses cookie, returns new access_token)
GET /healthResponse 200:
{ "status": "ok" }POST /auth/login
Content-Type: application/json
{
"user_email": "user@email.com",
"password": "mypassword123"
}Response 200:
{
"message": "login exitoso",
"access_token": "eyJhbGciOiJIUzI1NiIs..."
}Also sets
refresh_tokencookie (HttpOnly)
POST /auth/refresh
Cookie: refresh_token=<token>Response 200:
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600
}POST /users
Content-Type: application/json
{
"name": "John Doe",
"email": "john@email.com",
"password": "mypassword123",
"rol": "admin"
}Validations:
email: required, valid email formatpassword: required, 8-15 charactersrol: required, letters and spaces only, 4-10 characters
Response 201:
{
"id": 1,
"name": "John Doe",
"user_email": "john@email.com",
"rol": "admin",
"message": "usuario creado exitosamente",
"access_token": "eyJhbGciOiJIUzI1NiIs..."
}GET /users
Authorization: Bearer <access_token>Response 200:
{
"message": "se consulto el usuario exitosamente",
"user": {
"id": 1,
"name": "John Doe",
"user_email": "john@email.com",
"rol": "admin"
}
}PATCH /users
Authorization: Bearer <access_token>
Content-Type: application/json
{
"name": "John Smith",
"email": "johnsmith@email.com"
}Response 202:
{
"user_id": 1,
"message": "se actualizo exitosamente el usuario"
}DELETE /users
Authorization: Bearer <access_token>Response 202:
{
"message": "se elimino correctamente el usuario"
}All expense endpoints require
Authorization: Bearer <token>
POST /api/expenses
Authorization: Bearer <access_token>
Content-Type: application/json
{
"amount": 150.50,
"category": "Food",
"description": "Lunch at restaurant"
}Validations:
amount: required, numeric, >= 1category: required, letters and spaces only, 3-15 charactersdescription: required, letters and spaces only, 10-50 characters
Response 201:
{
"expense_id": 1,
"message": "se creo exitosamente el gasto"
}GET /api/expenses?id=1
Authorization: Bearer <access_token>Response 200:
{
"message": "se obtuvo exitosamente el gasto",
"expense": {
"id": 1,
"user_id": 1,
"amount": 150.50,
"category": "Food",
"description": "Lunch at restaurant",
"created_at": "2024-01-15T12:00:00Z",
"updated_at": "2024-01-15T12:00:00Z"
}
}PATCH /api/expenses?id=1
Authorization: Bearer <access_token>
Content-Type: application/json
{
"amount": 200.00,
"category": "Food",
"description": "Dinner at italian restaurant"
}Response 202:
{
"expense_id": 1,
"message": "se actualizo exitosamente el gasto"
}DELETE /api/expenses?id=1
Authorization: Bearer <access_token>Response 202:
{
"message": "se elimino correctamente el gasto"
}GET /api/expenses/reports?month=2024-01
Authorization: Bearer <access_token>Response 202:
{
"message": "reporte generado exitosamente",
"month": "2024-01",
"total": 1500.75
}| Code | Description |
|---|---|
| 400 | Invalid input data |
| 401 | Token not provided, expired or invalid |
| 403 | HTTPS required |
| 404 | Resource not found |
| 429 | Rate limit exceeded (3 req/20s per IP) |
| 500 | Internal server error |
Validation error example:
{
"errors": [
{
"field": "password",
"message": "must be between 8 and 15 characters"
}
]
}- β JWT Access Token (Bearer Header)
- β JWT Refresh Token (HttpOnly Cookie)
- β CORS configured (specific origins)
- β Security headers (HSTS, X-Frame-Options, CSP)
- β Rate Limiting (3 req/20s per IP)
- β Strict DTO validation
- β Passwords hashed with bcrypt
- β HTTPS required
expense-api/
βββ cmd/
β βββ api/main.go # Entry point & DI
β βββ middleware/ # HTTP middlewares
βββ internal/
β βββ adapter/
β β βββ handler/ # Primary adapters (HTTP controllers)
β β βββ repository/ # Secondary adapters (DB implementation)
β βββ core/
β β βββ domain/ # Business entities
β β βββ ports/ # Interfaces (contracts)
β βββ service/ # Application services (use cases)
βββ pkg/ # Shared utilities
| Pattern | Implementation | Location |
|---|---|---|
| Repository | Abstracts data access behind interfaces | internal/core/ports/ |
| Dependency Injection | Constructor injection for all services | cmd/api/main.go |
| DTO (Data Transfer Object) | Separates API contracts from domain | internal/adapter/handler/dto.go |
| Middleware Chain | Composable request processing | cmd/middleware/ |
| Factory | NewXxxService(), NewXxxHandler() constructors |
All layers |
| Principle | How it's applied |
|---|---|
| Separation of Concerns | Each layer has a single responsibility |
| Dependency Inversion | Services depend on interfaces, not implementations |
| Single Responsibility | One handler/service per domain entity |
| Clean Architecture | Domain has no external dependencies |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β HTTP Request β
βββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββ
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β MIDDLEWARES (cmd/middleware/) β
β β’ Auth (JWT validation) β
β β’ CORS β
β β’ Rate Limiting β
β β’ Security Headers β
β β’ HTTPS enforcement β
βββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββ
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β HANDLERS (internal/adapter/handler/) β
β β’ Request validation (DTOs) β
β β’ HTTP response formatting β
β β’ Error handling β
βββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββ
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β SERVICES (internal/service/) β
β β’ Business logic β
β β’ Orchestration β
β β’ Domain rules β
βββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββ
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PORTS (internal/core/ports/) β
β β’ Repository interfaces β
β β’ Contracts for adapters β
βββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββ
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β REPOSITORIES (internal/adapter/repository/) β
β β’ Database queries (Bun ORM) β
β β’ Data mapping β
βββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββ
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PostgreSQL β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
| Category | Feature | Status |
|---|---|---|
| Auth | JWT Access Token (Bearer) | β |
| Auth | JWT Refresh Token (HttpOnly Cookie) | β |
| Auth | Token refresh endpoint | β |
| Security | CORS (specific origins) | β |
| Security | Security Headers (HSTS, CSP, X-Frame-Options) | β |
| Security | HTTPS enforcement | β |
| Security | Rate Limiting (per IP) | β |
| Security | Password hashing (bcrypt) | β |
| Validation | DTO validation with custom validators | β |
| Validation | Centralized error handling | β |
| Database | Connection pooling | β |
| Database | Context with timeout | β |
| Logging | Structured logging (zerolog) | β |
| Logging | Request/Response logging middleware | β |
| Logging | Per-request context logging | β |
| Server | Graceful shutdown | β |
| Server | Health check endpoint | β |
// Connection pool settings
MaxOpenConns: 20 // Max simultaneous connections
MaxIdleConns: 5 // Connections kept idle
ConnMaxLifetime: 30 * time.Minute // Connection recycling- Gin - Web framework
- Bun - PostgreSQL ORM
- golang-jwt - JWT
- validator - Validation
- zerolog - Structured logging