Skip to content

DevLumuz/expense-api

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

8 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ’° Expense Tracker API

REST API for personal expense management with JWT authentication.

Project based on roadmap.sh/projects/expense-tracker

πŸ“‹ Postman Collection

View full collection


πŸš€ Quick Start

# Clone and install
git clone <repo-url>
cd expense-api

# Configure environment variables
cp .env.example .env

# Run
go run cmd/api/main.go

Environment Variables (.env)

PORT=:8080
GIN_MODE=debug
DATABASE_URL=postgres://user:pass@localhost:5432/expenses
JWT_SECRET=your-secure-secret

πŸ” Authentication

The 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

Authentication Flow

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)

πŸ“‘ Endpoints

Health Check

GET /health

Response 200:

{ "status": "ok" }

πŸ”‘ Auth

Login

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_token cookie (HttpOnly)

Refresh Token

POST /auth/refresh
Cookie: refresh_token=<token>

Response 200:

{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 3600
}

πŸ‘€ Users

Create User

POST /users
Content-Type: application/json

{
  "name": "John Doe",
  "email": "john@email.com",
  "password": "mypassword123",
  "rol": "admin"
}

Validations:

  • email: required, valid email format
  • password: required, 8-15 characters
  • rol: 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 User πŸ”’

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"
  }
}

Update User πŸ”’

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 User πŸ”’

DELETE /users
Authorization: Bearer <access_token>

Response 202:

{
  "message": "se elimino correctamente el usuario"
}

πŸ’Έ Expenses πŸ”’

All expense endpoints require Authorization: Bearer <token>

Create Expense

POST /api/expenses
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "amount": 150.50,
  "category": "Food",
  "description": "Lunch at restaurant"
}

Validations:

  • amount: required, numeric, >= 1
  • category: required, letters and spaces only, 3-15 characters
  • description: required, letters and spaces only, 10-50 characters

Response 201:

{
  "expense_id": 1,
  "message": "se creo exitosamente el gasto"
}

Get Expense

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"
  }
}

Update Expense

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 Expense

DELETE /api/expenses?id=1
Authorization: Bearer <access_token>

Response 202:

{
  "message": "se elimino correctamente el gasto"
}

Monthly Expense Report

GET /api/expenses/reports?month=2024-01
Authorization: Bearer <access_token>

Response 202:

{
  "message": "reporte generado exitosamente",
  "month": "2024-01",
  "total": 1500.75
}

⚠️ Common Errors

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"
    }
  ]
}

πŸ›‘οΈ Security

  • βœ… 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

πŸ—οΈ Architecture & Patterns

Hexagonal Architecture (Ports & Adapters)

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

Design Patterns

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

Architectural Principles

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

Layer Responsibilities

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                      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                              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Features Implemented

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 βœ…

Database Configuration

// Connection pool settings
MaxOpenConns:    20              // Max simultaneous connections
MaxIdleConns:    5               // Connections kept idle
ConnMaxLifetime: 30 * time.Minute // Connection recycling

πŸ“¦ Dependencies

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages