A full-stack web application for managing and swapping time slots between users. Built with Go backend and Vue.js frontend, SlotSwapper allows users to mark their calendar events as swappable and request swaps with other users.
- Overview
- Design Choices
- Project Structure
- Prerequisites
- Local Setup Instructions
- API Documentation
- Assumptions
- Challenges Faced
- Technology Stack
SlotSwapper is a time slot management and exchange platform that enables users to:
- Create and manage calendar events with specific time slots
- Mark events as swappable or busy
- Browse available swappable slots from other users
- Send swap requests to exchange time slots
- Accept or reject incoming swap requests
- Track the status of all swap requests (pending, accepted, rejected, cancelled)
The application uses JWT authentication for secure user sessions and provides a clean, responsive user interface built with Vue 3 and Tailwind CSS.
Framework: Go with Gin web framework for high performance and built-in middleware support.
Database: PostgreSQL for relational data with GORM as the ORM for type-safe database operations.
Authentication: JWT-based stateless authentication with tokens stored in HTTP-only cookies (when applicable) and localStorage for SPA usage.
API Design: RESTful API design with consistent response structure wrapping all responses in {message, data} format for predictable client-side handling.
Error Handling: Centralized error handling middleware that translates all errors into consistent JSON responses with appropriate HTTP status codes.
Transaction Management: Database transactions for operations that modify multiple tables (e.g., swap acceptance updates both events and swap request status).
Framework: Vue 3 with Composition API and TypeScript for type safety and modern reactive patterns.
State Management: Pinia stores for global state management with separate stores for authentication, events, swaps, and UI state.
Styling: Tailwind CSS v4 for utility-first styling with custom PostCSS configuration.
Routing: Vue Router with navigation guards for authentication and authorization checks.
API Communication: Axios with centralized configuration and request/response interceptors for token injection and error handling.
Component Design: Reusable component library including buttons, modals, toasts, loading spinners, and form components for consistency across the application.
Users Table: Stores user credentials with bcrypt-hashed passwords.
Events Table: Stores time slot information with status (BUSY or SWAPPABLE) and foreign key to user.
Swap Requests Table: Stores swap request information linking requester event and receiver event with status tracking (PENDING, ACCEPTED, REJECTED, CANCELLED).
- Passwords are hashed using bcrypt before storage
- JWT tokens expire after 24 hours
- CORS configuration allows only specified frontend origins
- Rate limiting middleware prevents abuse (100 requests per minute per IP)
- Input validation on all API endpoints
- SQL injection prevention through GORM parameterized queries
SlotSwapper/
├── backend/
│ ├── config/ # Database configuration
│ ├── controllers/ # HTTP request handlers
│ ├── middleware/ # Auth, CORS, logging, rate limiting
│ ├── models/ # Database models
│ ├── routes/ # Route definitions
│ ├── services/ # Business logic layer
│ ├── utils/ # Helper functions (JWT, errors)
│ ├── tests/ # Unit and integration tests
│ ├── go.mod # Go dependencies
│ └── main.go # Application entry point
├── frontend/
│ ├── src/
│ │ ├── api/ # API client modules
│ │ ├── components/ # Reusable Vue components
│ │ ├── composables/ # Vue composables
│ │ ├── router/ # Vue Router configuration
│ │ ├── stores/ # Pinia state stores
│ │ ├── types/ # TypeScript type definitions
│ │ ├── utils/ # Utility functions
│ │ └── views/ # Page components
│ ├── package.json # Node dependencies
│ └── vite.config.ts # Vite configuration
└── docker-compose.yaml # Docker orchestration
- Go 1.24 or higher
- Node.js 20.x or higher
- PostgreSQL 16.x or higher
- npm or yarn package manager
- Docker 20.x or higher
- Docker Compose 2.x or higher
git clone https://github.com/blue-samarth/SlotSwapper.git
cd SlotSwapperInstall and start PostgreSQL, then create the database:
# Connect to PostgreSQL
psql -U postgres
# Create database
CREATE DATABASE slotswapper;
# Exit psql
\qNavigate to the backend directory and install dependencies:
cd backend
go mod downloadCreate a .env file in the backend directory (optional, defaults will be used):
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=postgres
DB_NAME=slotswapper
JWT_SECRET=your-secret-key-change-in-production
PORT=8080Run database migrations (auto-migration runs on startup):
go run main.goThe backend will start on http://localhost:8080 and automatically create the required database tables.
Open a new terminal, navigate to the frontend directory, and install dependencies:
cd frontend
npm installCreate a .env file in the frontend directory (optional):
VITE_API_BASE_URL=http://localhost:8080/apiStart the development server:
npm run devThe frontend will start on http://localhost:5173.
Open your browser and navigate to http://localhost:5173. You can now:
- Sign up for a new account
- Log in with your credentials
- Create events with time slots
- Mark events as swappable
- Browse and request swaps from other users
Backend tests:
cd backend
go test ./tests/... -vFrontend tests (if configured):
cd frontend
npm run testIf you prefer Docker, you can run the entire stack with Docker Compose:
# Build and start all services
docker-compose up --build
# Access the application at http://localhost:80This will start:
- PostgreSQL database on port 5432
- Backend API on port 8080
- Frontend on port 80 (nginx)
For more details, see DOCKER.md.
http://localhost:8080/api
All API responses follow this structure:
{
"message": "Success message or error description",
"data": "Response data (object, array, or null)"
}Include JWT token in Authorization header for protected endpoints:
Authorization: Bearer <token>
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
| POST | /auth/signup |
Create new user account | No |
| POST | /auth/login |
Login and receive JWT token | No |
| POST | /auth/logout |
Logout current user | Yes |
| GET | /auth/me |
Get current user info | Yes |
Signup Request:
{
"username": "john_doe",
"email": "john@example.com",
"password": "securepassword123"
}Login Request:
{
"email": "john@example.com",
"password": "securepassword123"
}Login Response:
{
"message": "Login successful",
"data": {
"user": {
"id": 1,
"username": "john_doe",
"email": "john@example.com"
},
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
| GET | /events |
Get all events for current user | Yes |
| POST | /events |
Create new event | Yes |
| GET | /events/:id |
Get event by ID | Yes |
| PUT | /events/:id |
Update event | Yes |
| DELETE | /events/:id |
Delete event | Yes |
| PUT | /events/:id/status |
Toggle event status (BUSY/SWAPPABLE) | Yes |
Create Event Request:
{
"title": "Team Meeting",
"description": "Weekly team sync",
"start_time": "2025-11-10T14:00:00Z",
"end_time": "2025-11-10T15:00:00Z",
"location": "Conference Room A"
}Update Event Status Request:
{
"status": "SWAPPABLE"
}| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
| GET | /swaps/available |
Get all swappable slots from other users | Yes |
| POST | /swaps/initiate |
Initiate swap request | Yes |
| GET | /swaps/sent |
Get swap requests sent by current user | Yes |
| GET | /swaps/received |
Get swap requests received by current user | Yes |
| PUT | /swaps/:id/accept |
Accept swap request | Yes |
| PUT | /swaps/:id/reject |
Reject swap request | Yes |
| DELETE | /swaps/:id/cancel |
Cancel swap request | Yes |
Initiate Swap Request:
{
"requester_event_id": 5,
"receiver_event_id": 12
}Available Slots Response:
{
"message": "Swappable slots fetched successfully",
"data": [
{
"id": 12,
"title": "Office Hours",
"start_time": "2025-11-11T10:00:00Z",
"end_time": "2025-11-11T11:00:00Z",
"status": "SWAPPABLE",
"user": {
"id": 2,
"username": "jane_smith",
"email": "jane@example.com"
}
}
]
}| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
| GET | /users/:id |
Get user by ID | Yes |
| PUT | /users/:id |
Update user profile | Yes |
Update User Request:
{
"username": "new_username"
}| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
| GET | /health |
Check API health status | No |
-
Single Time Zone: All times are stored and handled in UTC. The frontend is responsible for displaying times in the user's local timezone.
-
Event Ownership: Users can only modify or delete their own events. Swap requests can only be initiated for events marked as SWAPPABLE.
-
Swap Request Lifecycle: A swap request can only be in one of four states: PENDING, ACCEPTED, REJECTED, or CANCELLED. Once accepted or rejected, the request cannot be modified.
-
Event Status Constraints: Events with pending swap requests cannot be deleted to maintain referential integrity. Users must cancel the swap request first.
-
One-to-One Swaps: Each swap request involves exactly two events: one from the requester and one from the receiver. Group swaps or multi-party swaps are not supported.
-
User Authentication: JWT tokens expire after 24 hours and must be refreshed by logging in again. No refresh token mechanism is implemented.
-
Email Uniqueness: Email addresses must be unique across all users. Usernames must also be unique.
-
Password Requirements: Minimum password length is 8 characters. No maximum length or complexity requirements are enforced at the API level (though the frontend validates for better UX).
-
Concurrent Requests: The system does not handle race conditions where two users might try to request a swap for the same event simultaneously. The first request to complete will succeed.
-
Event Overlap: The system does not prevent users from creating overlapping events. Users are responsible for managing their own schedules.
Challenge: Initially, the backend returned data directly, but later was changed to wrap responses in {message, data} structure. This broke the frontend which expected response.data to contain the actual data.
Solution: Updated all frontend API calls to access response.data.data and added null safety checks to handle cases where the backend returns {data: null}.
Challenge: When running in Docker containers, the frontend could not communicate with the backend due to CORS restrictions and different network contexts.
Solution: Implemented nginx reverse proxy in the frontend container to proxy /api/ requests to the backend container, eliminating cross-origin requests entirely.
Challenge: The backend always creates events with status BUSY regardless of what the frontend sends, but this was not documented or communicated to the frontend.
Solution: Removed the status field from the event creation form and added an informational note explaining that events are created as BUSY by default and can be toggled to SWAPPABLE afterwards.
Challenge: The backend sometimes returns null or undefined values for related entities (e.g., user field in events), causing "Cannot read properties of null" errors in the frontend.
Solution: Added comprehensive null safety checks throughout the frontend using optional chaining (?.), v-if guards before rendering, and fallback values (|| 'Unknown', || []) for all data access.
Challenge: After initiating a swap request, it would not appear in the "Sent" tab until the page was manually refreshed.
Solution: Added automatic data refresh calls (fetchSentRequests() and fetchEvents()) after successful swap initiation to update the UI immediately.
Challenge: The backend returns a 400 error when trying to delete an event that has pending swap requests, but no user-friendly error message was shown.
Solution: Added frontend validation to check event status before deletion and display a helpful toast warning message explaining that events with pending swaps cannot be deleted.
Challenge: Multiple developers working on the same files led to merge conflicts with duplicate code sections and conflict markers appearing in the files.
Solution: Carefully resolved conflicts by keeping the version with null safety improvements and bug fixes, then tested thoroughly to ensure no functionality was lost.
Challenge: Tailwind CSS v4 requires a PostCSS plugin configuration, which was not set up initially, causing no styles to load in the application.
Solution: Created postcss.config.js with the @tailwindcss/postcss plugin and installed the required npm package.
Challenge: Running tests would pollute the development database with test data, making it difficult to maintain clean test and development environments.
Solution: Implemented test helpers with transaction rollback for each test case, ensuring database state is not persisted between tests.
Challenge: During development, the rate limiter (100 requests per minute) would sometimes block legitimate development requests when rapidly testing features.
Solution: Made rate limiter configurable through environment variables and increased the limit during development. Production deployments use the stricter limit.
- Language: Go 1.24
- Web Framework: Gin 1.11
- Database: PostgreSQL 16
- ORM: GORM 1.31
- Authentication: JWT (golang-jwt/jwt v5.3)
- Password Hashing: bcrypt
- Environment Variables: godotenv 1.5
- Testing: Go standard testing package
- Framework: Vue 3.5
- Language: TypeScript 5.9
- Build Tool: Vite 7.1
- State Management: Pinia 3.0
- Routing: Vue Router 4.6
- HTTP Client: Axios 1.13
- Styling: Tailwind CSS 4.1
- CSS Processing: PostCSS 8.5 with Tailwind plugin
- Containerization: Docker
- Orchestration: Docker Compose
- Web Server: Nginx (for frontend in production)
- Database: PostgreSQL 16 Alpine
- Version Control: Git
- Code Editor: VS Code (recommended)
- API Testing: curl, Postman (optional)
- Database Client: psql, pgAdmin (optional)
This project is private and proprietary. All rights reserved.
For questions or issues, please contact the development team or create an issue in the repository.