A centralized platform for busy airports and malls to manage parking zones, specifically handling the high-demand reservation of limited EV charging spots.
Backend: https://spotsync-api-uzl2.onrender.com
| Technology | Package | Purpose |
|---|---|---|
| Go | go 1.22+ |
Programming language |
| Echo | github.com/labstack/echo/v4 |
High performance, minimalist web framework |
| GORM | gorm.io/gorm |
ORM for Go |
| PostgreSQL | gorm.io/driver/postgres |
Relational database (NeonDB) |
| Validator | github.com/go-playground/validator/v10 |
Struct validation, integrated with Echo |
| JWT | github.com/golang-jwt/jwt/v5 |
Standard token generation & verification |
| bcrypt | golang.org/x/crypto/bcrypt |
Password hashing |
| Godotenv | github.com/joho/godotenv |
Environment variable management |
| Air | github.com/air-verse/air |
Hot reloading during development |
- User registration (driver/admin roles)
- JWT-based authentication
- Role-based access control (driver, admin)
- Protected routes with middleware
- Create parking zones (admin only)
- View all zones with dynamic available spots calculation
- Support for zone types:
general,ev_charging,covered - Dynamic pricing per hour
- Reserve parking spots with concurrency-safe transactions
- Row-level locking (
FOR UPDATE) to prevent over-capacity - View personal reservations with zone details
- Cancel own reservations (driver)
- View all reservations (admin)
spotsync-api/
βββ cmd/
β βββ main.go # Application entry point
βββ internal/
β βββ auth/ # JWT service interface + implementation
β β βββ jwt.go
β βββ config/ # Configuration & database connection
β β βββ config.go
β β βββ db.go
β βββ domain/ # Domain modules (each with own layers)
β β βββ user/ # User/Auth domain
β β β βββ dto/
β β β β βββ request.go
β β β β βββ response.go
β β β βββ entity.go
β β β βββ repository.go
β β β βββ service.go
β β β βββ handler.go
β β β βββ register.go # Route registration + DI
β β βββ zone/ # Parking Zone domain
β β β βββ dto/
β β β β βββ request.go
β β β β βββ response.go
β β β βββ entity.go
β β β βββ repository.go
β β β βββ service.go
β β β βββ handler.go
β β β βββ register.go
β β βββ reservation/ # Reservation domain
β β βββ dto/
β β β βββ request.go
β β β βββ response.go
β β βββ entity.go
β β βββ repository.go
β β βββ service.go
β β βββ handler.go
β β βββ register.go
β βββ httpresponse/ # Standardized response helpers
β β βββ response.go
β β βββ error.go
β βββ middleware/ # Auth + Role middleware
β β βββ auth.go
β βββ server/ # HTTP server setup
β β βββ http.go
β βββ validation/ # Validation error parsing
β βββ error.go
βββ postman/ # Postman test guides
β βββ auth.postman.md
β βββ zone.postman.md
β βββ reservation.postman.md
βββ doc/ # Documentation
β βββ deploy.md
βββ ref-golang/ # Reference project (for learning)
βββ .env # Environment variables (not in git)
βββ .env.example # Example environment file
βββ .gitignore
βββ .air.toml # Air hot-reload config
βββ CONCEPTS.md # Project concepts & keywords
βββ go.mod
βββ go.sum
βββ README.md
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β HTTP Request β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Echo Server (server/http.go) β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Middleware: Logger β Recover β CORS β JWTAuth β Role β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Handler Layer β
β (Bind request β Validate DTO β Extract JWT claims β Call Serviceβ
β β Return JSON response) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Service Layer β
β (Business logic: Hash passwords, Generate JWT, Check capacity, β
β Enforce rules) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Repository Layer β
β (Database operations: CRUD, Transactions, Row Locks) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PostgreSQL (NeonDB) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Each domain wires its own dependencies in register.go:
Repository β Service β Handler β Routes
| Role | Allowed Actions |
|---|---|
| driver | β’ Register and log in β’ View all parking zones and availability β’ Reserve a parking/EV spot β’ View and cancel their own reservations |
| admin | β’ All driver permissions β’ Create parking zones β’ Set pricing for zones β’ View all reservations in the system |
| Field | Type | Constraints |
|---|---|---|
id |
SERIAL |
Primary Key, Auto-increment |
name |
VARCHAR(100) |
NOT NULL |
email |
VARCHAR(255) |
UNIQUE, NOT NULL |
password |
VARCHAR(255) |
NOT NULL (bcrypt hash) |
role |
VARCHAR(10) |
DEFAULT 'driver' |
created_at |
TIMESTAMP |
Auto-generated |
updated_at |
TIMESTAMP |
Auto-refreshed |
| Field | Type | Constraints |
|---|---|---|
id |
SERIAL |
Primary Key, Auto-increment |
name |
VARCHAR(100) |
NOT NULL |
type |
VARCHAR(20) |
NOT NULL (general, ev_charging, covered) |
total_capacity |
INTEGER |
NOT NULL, > 0 |
price_per_hour |
DECIMAL |
NOT NULL, > 0 |
created_at |
TIMESTAMP |
Auto-generated |
updated_at |
TIMESTAMP |
Auto-refreshed |
| Field | Type | Constraints |
|---|---|---|
id |
SERIAL |
Primary Key, Auto-increment |
user_id |
INTEGER |
Foreign Key β users.id |
zone_id |
INTEGER |
Foreign Key β parking_zones.id |
license_plate |
VARCHAR(15) |
NOT NULL |
status |
VARCHAR(15) |
DEFAULT 'active' (active, completed, cancelled) |
created_at |
TIMESTAMP |
Auto-generated |
updated_at |
TIMESTAMP |
Auto-refreshed |
http://localhost:8080/api/v1
| Method | URL | Access |
|---|---|---|
GET |
/health |
Public |
| Method | URL | Access | Description |
|---|---|---|---|
POST |
/api/v1/auth/register |
Public | Register new user |
POST |
/api/v1/auth/login |
Public | Login user |
GET |
/api/v1/auth/me |
Authenticated | Get current user info |
| Method | URL | Access | Description |
|---|---|---|---|
GET |
/api/v1/zones |
Public | Get all zones |
GET |
/api/v1/zones/:id |
Public | Get zone by ID |
POST |
/api/v1/zones |
Admin | Create new zone |
| Method | URL | Access | Description |
|---|---|---|---|
POST |
/api/v1/reservations |
Authenticated | Create reservation |
GET |
/api/v1/reservations/my-reservations |
Authenticated | Get my reservations |
DELETE |
/api/v1/reservations/:id |
Authenticated | Cancel reservation |
GET |
/api/v1/reservations |
Admin | Get all reservations |
All success responses follow a standardized wrapper format:
{
"success": true,
"message": "Operation description",
"data": { ... }
}| Field | Type | Description |
|---|---|---|
success |
boolean |
Always true for success |
message |
string |
Human-readable success message |
data |
object/array |
Response data (DTO object) |
Example - Register User (201 Created):
{
"success": true,
"message": "User registered successfully",
"data": {
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"role": "driver",
"created_at": "2026-06-29T10:00:00+06:00",
"updated_at": "2026-06-29T10:00:00+06:00"
}
}Example - Login (200 OK):
{
"success": true,
"message": "Login successful",
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"role": "driver"
}
}
}Example - Get All Zones (200 OK):
{
"success": true,
"message": "Parking zones retrieved successfully",
"data": [
{
"id": 1,
"name": "Terminal 1 EV Charging",
"type": "ev_charging",
"total_capacity": 20,
"available_spots": 14,
"price_per_hour": 5.5,
"created_at": "2026-06-29T10:30:00+06:00"
}
]
}All error responses follow a standardized wrapper format:
{
"success": false,
"message": "Error description",
"errors": "Error details or validation errors"
}| Field | Type | Description |
|---|---|---|
success |
boolean |
Always false for errors |
message |
string |
Human-readable error message |
errors |
string/object/null |
Error details (string for simple errors, object for validation errors, null for no details) |
Example - Validation Error (400 Bad Request):
{
"success": false,
"message": "Validation failed",
"errors": {
"errors": [
{
"field": "Email",
"message": "Email must be a valid email address"
},
{
"field": "Password",
"message": "Password must be at least 6 characters"
}
]
}
}Example - Authentication Error (401 Unauthorized):
{
"success": false,
"message": "Missing authorization header",
"errors": null
}Example - Forbidden Error (403 Forbidden):
{
"success": false,
"message": "Admin access required",
"errors": null
}Example - Not Found Error (404 Not Found):
{
"success": false,
"message": "Parking zone not found",
"errors": null
}Example - Conflict Error (409 Conflict):
{
"success": false,
"message": "Parking zone is full",
"errors": null
}| Code | Usage |
|---|---|
200 |
Successful GET, DELETE |
201 |
Successful POST (resource created) |
400 |
Validation errors, invalid input, duplicate resource |
401 |
Missing, expired, or invalid JWT token |
403 |
Valid token but insufficient role/permissions |
404 |
Requested resource does not exist |
409 |
Business logic conflict (e.g., Zone is full) |
500 |
Unexpected server or database error |
- Go 1.22 or higher
- PostgreSQL (NeonDB, Supabase, or local)
- Git
git clone https://github.com/ishtiaqrobin/spotsync-api.git
cd spotsync-apigo mod downloadCreate a .env file in the root directory:
# Option 1: Direct DSN (recommended for NeonDB)
DSN=postgresql://user:password@host.neon.tech/dbname?sslmode=require
# Option 2: Individual DB components
DB_HOST=your-neondb-host.neon.tech
DB_PORT=5432
DB_USER=your-db-user
DB_PASSWORD=your-db-password
DB_NAME=spotsync
DB_SSLMODE=require
# JWT Secret (generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))")
JWT_SECRET=your-super-secret-key
# Server Port
PORT=8080Without hot-reload:
go run ./cmd/main.goWith hot-reload (Air):
# Install air if not already installed
go install github.com/air-verse/air@latest
# Run with air
airVisit http://localhost:8080/health β you should see:
{
"status": "ok"
}Detailed Postman test guides are available in the postman/ directory:
postman/auth.postman.mdβ Authentication endpointspostman/zone.postman.mdβ Parking zone endpointspostman/reservation.postman.mdβ Reservation endpoints
| Variable | Description |
|---|---|
base_url |
http://localhost:8080/api/v1 |
driver_token |
Set after driver login |
admin_token |
Set after admin login |
- Password Hashing: bcrypt with default cost (10)
- JWT Authentication: HS256 signed tokens with 24-hour expiry
- Role-Based Access Control: Middleware enforces admin-only routes
- Password Never Exposed:
json:"-"tag prevents password in responses - CORS Enabled: Cross-origin requests supported
The reservation system uses GORM Transactions with Row-Level Locking (FOR UPDATE) to prevent the "EV Spot Bottleneck" race condition:
db.Transaction(func(tx *gorm.DB) error {
// 1. Lock the zone row
tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&zone, zoneID)
// 2. Count active reservations
tx.Model(&Reservation{}).Where("zone_id = ? AND status = ?", zoneID, "active").Count(&count)
// 3. Check capacity
if activeCount >= zone.TotalCapacity {
return ErrZoneFull
}
// 4. Create reservation
tx.Create(&reservation)
return nil
})- Push code to GitHub
- Connect repository to Render/Railway/Fly.io
- Set environment variables in dashboard
- Deploy
- Go to neon.tech and create a project
- Copy the connection string
- Set as
DSNin your environment variables
This project is built for educational purposes as part of the Level2-B6 Mission-9 Assignment.
Ishtiaq Robin
- GitHub: @ishtiaqrobin
- Project: spotsync-api
- Live: spotsync-api
Built using Go, Echo, and GORM