The definitive backend solution for collaborative trip planning.
Built with NestJS, Prisma, and PostgreSQL.
Welcome to the Trip-Mate backend repository! This project provides a robust, scalable, and secure API for the Trip-Mate application, a platform designed to make planning group trips seamless and enjoyable. This document will guide you through every aspect of the project, making it a cake walk to get started, regardless of your experience with NestJS.
- β¨ Core Features
- π οΈ Tech Stack
- π Project Structure
- π Getting Started
- π API Authentication Flow
- π API Endpoint Documentation
- π§ͺ Running Tests
- π€ Contributing
- π License
- π Secure Authentication: JWT-based authentication with email verification via OTP
- βοΈ Email Verification: 6-digit OTP sent to email for secure user registration
- π₯ User Management: Complete user profile management with role-based access
- πΊοΈ Tour Management: Browse, create, and manage tours with detailed itineraries
- π Booking System: Book tours with status tracking and payment management
- π¬ Community Features: Share experiences through posts and comments
- π Real-time Notifications: Stay updated with booking confirmations and updates
- π° Expense Tracking: Track and split expenses transparently
- π± Mobile-Ready API: RESTful API design optimized for mobile applications
- Framework: NestJS - Progressive Node.js framework
- Language: TypeScript - Type-safe JavaScript
- ORM: Prisma - Next-generation TypeScript ORM
- Database: PostgreSQL - Powerful open-source database
- Authentication: Passport.js with JWT Strategy
- Email Service: Nodemailer with Brevo SMTP
- Validation: class-validator
- API Documentation: Swagger/OpenAPI
The project follows a standard modular architecture. Understanding the structure is key to understanding the application's flow.
.
βββ prisma/
β βββ migrations/ # Contains SQL migration files generated by Prisma.
β βββ schema.prisma # The single source of truth for your database schema and models.
β
βββ src/
β βββ auth/ # Handles all authentication logic (login, register).
β β βββ dto/ # Data Transfer Objects defining the shape of auth-related request bodies.
β β βββ guards/ # Route guards, like `JwtAuthGuard` to protect endpoints.
β β βββ auth.controller.ts # Defines the `/auth` API routes.
β β βββ auth.service.ts # Contains the business logic for authentication.
β β βββ jwt.strategy.ts # Implements the logic to validate JWTs.
| |
| βββ mail/
| | βββ mail.service.ts # Email sending logic
| | βββ mail.module.ts # Mail module configuration
β β
β βββ users/ # Manages user-related operations (e.g., fetching profiles).
β β βββ users.controller.ts # Defines the `/users` API routes.
β β βββ users.service.ts # Business logic for user operations.
β β
β βββ trips/ # The core module for all trip-related functionality.
β β βββ dto/ # DTOs for trips, destinations, activities, and expenses.
β β βββ trips.controller.ts # Defines all `/trips` routes and sub-routes.
β β βββ trips.service.ts # Contains all business logic for managing trips and their related entities.
β β
β βββ common/ # Shared utilities.
β β βββ decorators/ # Custom decorators (e.g., `@GetUser` to inject user object into requests).
β β
β βββ app.module.ts # The root module that ties all other modules together.
β βββ main.ts # The application's entry point. It creates and starts the NestJS app.
β
βββ .env.example # Template for your environment variables.
βββ package.json # Lists project dependencies and scripts.
βββ tsconfig.json # Configuration for the TypeScript compiler.
Follow these steps to get a local copy of the project up and running.
- Node.js (v20 or newer)
- npm or Yarn
- PostgreSQL (v15 or newer)
- Docker (optional, for containerized setup)
- Brevo Account (for email service)
-
Clone the Repository
git clone https://github.com/KBLReddy/trip-mate-backend.git cd trip-mate-backend -
Install Dependencies
npm install
-
Set Up Environment Variables
cp .env.example .env
Edit
.envwith your configuration:# Application NODE_ENV=development PORT=3000 # Database DATABASE_URL="postgresql://postgres:password@localhost:5432/tripmate?schema=public" DIRECT_URL="postgresql://postgres:password@localhost:5432/tripmate?schema=public" # JWT Secrets (generate strong secrets for production) JWT_SECRET="your-super-secret-jwt-key" JWT_REFRESH_SECRET="your-super-secret-refresh-key" JWT_EXPIRES_IN="15m" JWT_REFRESH_EXPIRES_IN="7d" # Email Configuration (Brevo) MAIL_HOST=smtp-relay.brevo.com MAIL_PORT=587 MAIL_USER=your-email@gmail.com MAIL_PASS=xkeysib-your-brevo-smtp-key MAIL_FROM_NAME=TripMate MAIL_FROM_EMAIL=noreply@tripmate.com # Frontend URL (for CORS) FRONTEND_URL=http://localhost:3000 CORS_ORIGINS="http://localhost:3000,http://localhost:3001"
Important: Ensure you have a PostgreSQL database created that matches the
DATABASE_NAMEin yourDATABASE_URL. -
Apply Database Migrations This command reads your
prisma/schema.prismafile and creates or updates the database tables accordingly.npx prisma migrate dev
This will also generate the Prisma Client, a type-safe query builder for your database.
-
Development Mode This command starts the server with hot-reloading, which automatically restarts the server when you save a file.
npm run start:dev
The API will be available at
http://localhost:3000. -
Production Mode To build and run the application for production:
npm run build npm run start:prod
The API uses JWT-based authentication with email verification:
- User registers with email, password, and name
- System sends OTP (6-digit code) to user's email
- User verifies OTP within 10 minutes
- System returns JWT tokens (access & refresh tokens)
For protected endpoints, include the access token:
Authorization: Bearer <access_token>
sequenceDiagram
participant User
participant API
participant Email Service
User->>API: POST /auth/register
API->>Email Service: Send OTP
Email Service->>User: Email with 6-digit OTP
User->>API: POST /auth/verify-otp
API->>User: JWT tokens + User data
Note:
- All endpoints marked as π Protected require you to include your access token in the
Authorizationheader of every request:Authorization: Bearer <access_token>- You receive the
access_token(andrefresh_token) after logging in or registering.- Refresh tokens are only used for
/auth/refreshand/auth/logoutendpoints. For those, use:and include the refresh token in the request body as well.Authorization: Bearer <refresh_token>- If you omit the access token for protected endpoints, you will receive a
401 Unauthorizederror.
- Endpoint:
POST /auth/register - Description: Register a new user and send OTP to email
- Authentication: π Public
- Request Body:
{ "email": "user@example.com", "password": "StrongPassword123!", "name": "John Doe" } - Success Response (201):
{ "message": "Verification code sent to your email", "userId": "123e4567-e89b-12d3-a456-426614174000", "email": "user@example.com", "expiresIn": "10 minutes" }
- Endpoint:
POST /auth/verify-otp - Description: Verify email with OTP and get tokens
- Authentication: π Public
- Request Body:
{ "userId": "123e4567-e89b-12d3-a456-426614174000", "otp": "123456" } - Success Response (200):
{ "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "user": { "id": "123e4567-e89b-12d3-a456-426614174000", "email": "user@example.com", "name": "John Doe", "role": "USER" } }
- Endpoint:
POST /auth/resend-otp - Description: Resend OTP to user's email (rate limited to 1 per minute)
- Authentication: π Public
- Request Body:
{ "userId": "123e4567-e89b-12d3-a456-426614174000" } - Success Response (200):
{ "message": "New verification code sent to your email", "expiresIn": "10 minutes" }
- Endpoint:
POST /auth/login - Description: Authenticates a user with their email and password.
- Authentication: π Public
- Request Body:
application/json{ "email": "jane.doe@example.com", "password": "a-very-strong-password-123!" } - Success Response (200 OK):
{ "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "user": { "id": "user-uuid-123", "email": "user@tripmate.com", "name": "Test User", "role": "USER" } } - Error Response (401 Unauthorized): If credentials are invalid.
- Endpoint:
POST /auth/refresh - Description: Refreshes the access token using a valid refresh token.
- Authentication: π Requires valid refresh token in
Authorizationheader. - Request Body:
application/json{ "refreshToken": "<your_refresh_token>" } - Headers:
Authorization: Bearer <refresh_token> - Success Response (200 OK):
{ "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "user": { "id": "user-uuid-123", "email": "user@tripmate.com", "name": "Test User", "role": "USER" } } - Error Response (403 Forbidden): If the refresh token is invalid or expired.
- Endpoint:
POST /auth/logout - Description: Logs out the user and invalidates the refresh token.
- Authentication: π Requires valid access token in
Authorizationheader. - Request Body:
application/json{ "refreshToken": "<your_refresh_token>" } - Headers:
and in the body:
Authorization: Bearer <access_token>{ "refreshToken": "<refresh_token>" } - Success Response (200 OK): Empty response.
- JWT (JSON Web Token) is used for stateless authentication. After login, the server issues an
accessToken(short-lived) and arefreshToken(longer-lived). - Access Token: Used in the
Authorizationheader for protected endpoints. Expires quickly (e.g., 15 minutes). - Refresh Token: Used to obtain a new access token when the old one expires. Should be stored securely (e.g., HTTP-only cookie or secure storage).
- Logout: Always send the refresh token to invalidate it on the server.
Example: Login, Use, Refresh, and Logout
// Login
fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'user@example.com', password: 'password123' })
})
.then(res => res.json())
.then(({ accessToken, refreshToken }) => {
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
});
// Use access token
fetch('/api/users/me', {
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('accessToken') }
});
// Refresh token
fetch('/api/auth/refresh', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('refreshToken')
},
body: JSON.stringify({ refreshToken: localStorage.getItem('refreshToken') })
})
.then(res => res.json())
.then(({ accessToken, refreshToken }) => {
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
});
// Logout
fetch('/api/auth/logout', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('accessToken')
},
body: JSON.stringify({ refreshToken: localStorage.getItem('refreshToken') })
});// Retrofit API interface
interface AuthApi {
@POST("/api/auth/login")
suspend fun login(@Body body: LoginRequest): AuthResponse
@POST("/api/auth/refresh")
suspend fun refresh(
@Header("Authorization") refreshToken: String,
@Body body: RefreshRequest
): AuthResponse
@POST("/api/auth/logout")
suspend fun logout(
@Header("Authorization") accessToken: String,
@Body body: RefreshRequest
): Response<Unit>
@GET("/api/users/me")
suspend fun getProfile(@Header("Authorization") accessToken: String): UserProfile
}
data class LoginRequest(val email: String, val password: String)
data class RefreshRequest(val refreshToken: String)
data class AuthResponse(val accessToken: String, val refreshToken: String, val user: UserProfile)
data class UserProfile(val id: String, val email: String, val name: String, val role: String)
// Usage in a ViewModel or Repository
suspend fun loginAndStoreTokens(api: AuthApi, email: String, password: String, context: Context) {
val response = api.login(LoginRequest(email, password))
// Store tokens securely (e.g., DataStore, EncryptedSharedPreferences)
saveToken(context, "accessToken", response.accessToken)
saveToken(context, "refreshToken", response.refreshToken)
}
suspend fun getProfile(api: AuthApi, context: Context): UserProfile {
val accessToken = getToken(context, "accessToken")
return api.getProfile("Bearer $accessToken")
}
suspend fun refreshToken(api: AuthApi, context: Context) {
val refreshToken = getToken(context, "refreshToken")
val response = api.refresh("Bearer $refreshToken", RefreshRequest(refreshToken))
saveToken(context, "accessToken", response.accessToken)
saveToken(context, "refreshToken", response.refreshToken)
}
suspend fun logout(api: AuthApi, context: Context) {
val accessToken = getToken(context, "accessToken")
val refreshToken = getToken(context, "refreshToken")
api.logout("Bearer $accessToken", RefreshRequest(refreshToken))
clearTokens(context)
}
// Helper functions (pseudo-code)
fun saveToken(context: Context, key: String, value: String) { /* ... */ }
fun getToken(context: Context, key: String): String { /* ... */ return "" }
fun clearTokens(context: Context) { /* ... */ }Handles user-specific data.
- Endpoint:
GET /users/me - Description: Retrieves the profile information of the currently authenticated user.
- Authentication: π Protected
- Headers:
Authorization: Bearer <access_token> - Success Response (200 OK):
{ "id": "clxza1b2c3d4e5f6", "email": "jane.doe@example.com", "name": "Jane Doe" } - Error Response (401 Unauthorized): If the token is missing or invalid.
- Endpoint:
PUT /users/me - Description: Updates the current user's profile.
- Authentication: π Protected
- Headers:
Authorization: Bearer <access_token> - Request Body:
application/json(optional fields){ "name": "Jane Doe Updated", "avatar": "https://example.com/avatar.jpg" } - Success Response (200 OK): The updated user object.
- Error Response (401 Unauthorized): If the token is missing or invalid.
- Endpoint:
PUT /users/me/password - Description: Changes the current user's password.
- Authentication: π Protected
- Request Body:
application/json{ "currentPassword": "old-password", "newPassword": "new-password" } - Success Response (200 OK): Empty response.
- Error Response (401 Unauthorized): If the token is missing or invalid.
- Error Response (400 Bad Request): If current password is incorrect.
- Endpoint:
GET /users - Description: Retrieves a list of all users.
- Authentication: π Admin only
- Success Response (200 OK): An array of user objects.
[ { "id": "user-uuid-123", "email": "user@tripmate.com", "name": "Test User", "role": "USER", "createdAt": "2024-07-20T10:00:00.000Z" } ]
- Endpoint:
GET /users/:id - Description: Retrieves a specific user by ID.
- Authentication: π Admin only
- Path Parameters:
id(string): The ID of the user.
- Success Response (200 OK): The user object.
- Endpoint:
PUT /users/:id - Description: Updates a specific user's details.
- Authentication: π Admin only
- Path Parameters:
id(string): The ID of the user.
- Request Body:
application/json(optional fields){ "name": "Admin User Updated", "role": "ADMIN" } - Success Response (200 OK): The updated user object.
- Endpoint:
DELETE /users/:id - Description: Deletes a specific user.
- Authentication: π Admin only
- Path Parameters:
id(string): The ID of the user.
- Success Response (204 No Content): An empty response body.
The core module for managing trips and all related data.
- Endpoint:
POST /trips - Description: Creates a new trip. The user making the request automatically becomes the owner and a member of the trip.
- Authentication: π Protected
- Request Body:
application/json{ "name": "Summer Trip to Japan", "description": "A 10-day adventure exploring Tokyo, Kyoto, and Osaka.", "startDate": "2025-07-20T00:00:00.000Z", "endDate": "2025-07-30T00:00:00.000Z" }startDate&endDatemust be in ISO 8601 format.
Sample Request:
{
"name": "Summer Trip to Japan",
"description": "A 10-day adventure exploring Tokyo, Kyoto, and Osaka.",
"startDate": "2025-07-20T00:00:00.000Z",
"endDate": "2025-07-30T00:00:00.000Z"
}Sample Response (201 Created):
{
"id": "trip_abc123",
"name": "Summer Trip to Japan",
"description": "A 10-day adventure exploring Tokyo, Kyoto, and Osaka.",
"startDate": "2025-07-20T00:00:00.000Z",
"endDate": "2025-07-30T00:00:00.000Z",
"ownerId": "user_xyz789",
"createdAt": "2025-07-01T10:00:00.000Z",
"updatedAt": "2025-07-01T10:00:00.000Z"
}- Endpoint:
GET /trips - Description: Retrieves a list of all trips that the authenticated user is a member of.
- Authentication: π Protected
- Success Response (200 OK): An array of trip objects.
[ { "id": "trip_abc123", "name": "Summer Trip to Japan", "description": "...", "startDate": "...", "endDate": "...", "ownerId": "user_xyz789" } ]
Sample Response (200 OK):
[
{
"id": "trip_abc123",
"name": "Summer Trip to Japan",
"description": "A 10-day adventure exploring Tokyo, Kyoto, and Osaka.",
"startDate": "2025-07-20T00:00:00.000Z",
"endDate": "2025-07-30T00:00:00.000Z",
"ownerId": "user_xyz789"
}
]Advanced Example: Empty List
[]- Endpoint:
GET /trips/:id - Description: Fetches all details for a specific trip, including members, destinations, activities, and expenses. The user must be a member of the trip to view it.
- Authentication: π Protected
- Path Parameters:
id(string): The unique identifier of the trip.
- Success Response (200 OK):
{ "id": "trip_abc123", "name": "Summer Trip to Japan", "description": "...", "members": [ { "user": { "id": "user_xyz789", "name": "Jane Doe", "email": "jane.doe@example.com" } } ], "destinations": [], "activities": [], "expenses": [] }
Sample Response (200 OK):
{
"id": "trip_abc123",
"name": "Summer Trip to Japan",
"description": "A 10-day adventure exploring Tokyo, Kyoto, and Osaka.",
"members": [
{ "user": { "id": "user_xyz789", "name": "Jane Doe", "email": "jane.doe@example.com" } }
],
"destinations": [],
"activities": [],
"expenses": []
}Advanced Example: Not Found (404)
{
"statusCode": 404,
"message": "Trip not found",
"error": "Not Found"
}- Endpoint:
PUT /trips/:id - Description: Updates a trip's details. Only the trip's owner can perform this action.
- Authentication: π Protected
- Path Parameters:
id(string): The ID of the trip to update.
- Request Body:
application/json(fields are optional){ "name": "An Epic Summer Trip to Japan", "description": "Updated description with more details." }
Sample Request:
{
"name": "An Epic Summer Trip to Japan",
"description": "Updated description with more details."
}Sample Response (200 OK):
{
"id": "trip_abc123",
"name": "An Epic Summer Trip to Japan",
"description": "Updated description with more details.",
"startDate": "2025-07-20T00:00:00.000Z",
"endDate": "2025-07-30T00:00:00.000Z",
"ownerId": "user_xyz789",
"createdAt": "2025-07-01T10:00:00.000Z",
"updatedAt": "2025-07-02T10:00:00.000Z"
}Advanced Example: Forbidden (403)
{
"statusCode": 403,
"message": "You are not the owner of this trip",
"error": "Forbidden"
}- Endpoint:
DELETE /trips/:id - Description: Deletes a trip and all its associated data (members, destinations, etc.). Only the trip's owner can perform this action.
- Authentication: π Protected
- Path Parameters:
id(string): The ID of the trip to delete.
- Success Response (204 No Content): An empty response body.
Sample Response (204 No Content):
{}Advanced Example: Forbidden (403)
{
"statusCode": 403,
"message": "You are not the owner of this trip",
"error": "Forbidden"
}- Endpoint:
POST /trips/:id/members - Description: Adds another registered user to the trip as a member. Only existing trip members can add new members.
- Authentication: π Protected
- Path Parameters:
id(string): The ID of the trip.
- Request Body:
{ "email": "friend@example.com" }
Sample Request:
{
"email": "friend@example.com"
}Sample Response (201 Created):
{
"id": "trip_abc123",
"name": "Summer Trip to Japan",
"members": [
{ "user": { "id": "user_xyz789", "name": "Jane Doe", "email": "jane.doe@example.com" } },
{ "user": { "id": "friend_123", "name": "Friend User", "email": "friend@example.com" } }
]
}Advanced Example: User Not Found (404)
{
"statusCode": 404,
"message": "User with email friend@example.com not found",
"error": "Not Found"
}- Endpoint:
DELETE /trips/:id/members/:userId - Description: Removes a member from a trip. Only the trip owner can remove members. The owner cannot remove themselves.
- Authentication: π Protected
- Path Parameters:
id(string): The ID of the trip.userId(string): The ID of the user to remove.
- Success Response (200 OK): The full trip object with the updated member list.
Sample Response (200 OK):
{
"id": "trip_abc123",
"name": "Summer Trip to Japan",
"members": [
{ "user": { "id": "user_xyz789", "name": "Jane Doe", "email": "jane.doe@example.com" } }
]
}Advanced Example: Bad Request (400)
{
"statusCode": 400,
"message": "Cannot remove the owner of the trip",
"error": "Bad Request"
}Manage the places you'll visit on your trip.
- Endpoint:
POST /trips/:id/destinations - Description: Adds a new destination to the trip's itinerary. Only members of the trip can add destinations.
- Authentication: π Protected
- Path Parameters:
id(string): The ID of the trip.
- Request Body:
application/json{ "name": "Kyoto", "country": "Japan", "arrivalDate": "2024-07-25T00:00:00.000Z", "departureDate": "2024-07-28T00:00:00.000Z" } - Success Response (201 Created): The newly created destination object.
{ "id": "dest_def456", "name": "Kyoto", "country": "Japan", "arrivalDate": "2024-07-25T00:00:00.000Z", "departureDate": "2024-07-28T00:00:00.000Z", "tripId": "trip_abc123" }
- Endpoint:
PUT /trips/:tripId/destinations/:destinationId - Description: Updates the details of a destination. Only trip members can update.
- Authentication: π Protected
- Path Parameters:
tripId(string): The ID of the trip.destinationId(string): The ID of the destination to update.
- Request Body:
application/json(fields are optional){ "name": "Kyoto (Ancient Capital)" } - Success Response (200 OK): The updated destination object.
- Endpoint:
DELETE /trips/:tripId/destinations/:destinationId - Description: Deletes a destination from a trip. Only trip members can delete.
- Authentication: π Protected
- Path Parameters:
tripId(string): The ID of the trip.destinationId(string): The ID of the destination to delete.
- Success Response (204 No Content): An empty response body.
Manage the activities you'll do at each destination.
- Endpoint:
POST /trips/:tripId/destinations/:destinationId/activities - Description: Adds a new activity to a specific destination within a trip. Only trip members can add activities.
- Authentication: π Protected
- Path Parameters:
tripId(string): The ID of the trip.destinationId(string): The ID of the destination for this activity.
- Request Body:
application/json{ "name": "Visit Fushimi Inari Shrine", "date": "2024-07-26T09:00:00.000Z", "description": "Hike through the thousands of torii gates." } - Success Response (201 Created): The newly created activity object.
{ "id": "act_ghi789", "name": "Visit Fushimi Inari Shrine", "date": "2024-07-26T09:00:00.000Z", "description": "Hike through the thousands of torii gates.", "destinationId": "dest_def456" }
- Endpoint:
PUT /trips/:tripId/activities/:activityId - Description: Updates the details of an activity. Only trip members can update.
- Authentication: π Protected
- Path Parameters:
tripId(string): The ID of the trip.activityId(string): The ID of the activity to update.
- Request Body:
application/json(fields are optional){ "description": "Early morning hike to avoid the crowds." } - Success Response (200 OK): The updated activity object.
- Endpoint:
DELETE /trips/:tripId/activities/:activityId - Description: Deletes an activity from a trip. Only trip members can delete.
- Authentication: π Protected
- Path Parameters:
tripId(string): The ID of the trip.activityId(string): The ID of the activity to delete.
- Success Response (204 No Content): An empty response body.
Manage and track shared expenses for a trip.
-
Endpoint:
POST /trips/:id/expenses -
Description: Logs a new expense for the trip. The user making the request is automatically assigned as the
paidById. Only trip members can add expenses. -
Authentication: π Protected
-
Path Parameters:
id(string): The ID of the trip.
-
Request Body:
application/json{ "description": "Shinkansen Tickets (Tokyo to Kyoto)", "amount": 130.50, "category": "TRANSPORTATION" }categorymust be one of the following:FOOD,TRANSPORTATION,ACCOMMODATION,ACTIVITIES,OTHER.
-
Success Response (201 Created): The newly created expense object.
{ "id": "exp_jkl012", "description": "Shinkansen Tickets (Tokyo to Kyoto)", "amount": 130.5, "category": "TRANSPORTATION", "date": "2023-10-27T10:00:00.000Z", "tripId": "trip_abc123", "paidById": "user_xyz789" }
- Endpoint:
PUT /trips/:tripId/expenses/:expenseId - Description: Updates the details of an expense. Only the user who originally added the expense can update it.
- Authentication: π Protected
- Path Parameters:
tripId(string): The ID of the trip.expenseId(string): The ID of the expense to update.
- Request Body:
application/json(fields are optional){ "amount": 135.00 } - Success Response (200 OK): The updated expense object.
- Error Response (403 Forbidden): If the user is not the one who paid for the expense.
- Endpoint:
DELETE /trips/:tripId/expenses/:expenseId - Description: Deletes an expense from a trip. Only the user who added the expense can delete it.
- Authentication: π Protected
- Path Parameters:
tripId(string): The ID of the trip.expenseId(string): The ID of the expense to delete.
- Success Response (204 No Content): An empty response body.
- Error Response (403 Forbidden): If the user is not the one who paid for the expense.
Manage tours, search, statistics, and categories.
- Endpoint:
POST /tours - Description: Creates a new tour. Only Admins and Guides can create tours.
- Authentication: π Protected (Admin/Guide)
- Request Body:
{ "title": "Amazing Bali Adventure", "description": "Experience the beauty of Bali with our 7-day adventure tour including temples, beaches, and rice terraces.", "location": "Bali, Indonesia", "price": 1299.99, "startDate": "2025-06-01T00:00:00.000Z", "endDate": "2025-06-07T00:00:00.000Z", "capacity": 20, "imageUrl": "https://images.unsplash.com/photo-1537996194471-e657df975ab4", "category": "adventure" } - Success Response (201 Created): Tour object.
- Endpoint:
GET /tours - Description: List all tours with filters and pagination.
- Authentication: π Public
- Query Parameters:
search(string): Search by title/locationcategory(string)location(string)minPrice(number)maxPrice(number)page(number, default 1)limit(number, default 10)sortBy(string: price, startDate, createdAt)sortOrder(asc|desc)
Sample Response (200 OK):
{
"data": [
{
"id": "tour-uuid-123",
"title": "Amazing Bali Adventure",
"description": "Experience the beauty of Bali with our 7-day adventure tour including temples, beaches, and rice terraces.",
"location": "Bali, Indonesia",
"price": 1299.99,
"startDate": "2025-06-01T00:00:00.000Z",
"endDate": "2025-06-07T00:00:00.000Z",
"capacity": 20,
"imageUrl": "https://images.unsplash.com/photo-1537996194471-e657df975ab4",
"category": "adventure",
"guide": {
"id": "guide-uuid-123",
"name": "Tour Guide",
"email": "guide@tripmate.com"
},
"createdAt": "2024-07-20T10:00:00.000Z",
"updatedAt": "2024-07-20T10:00:00.000Z"
}
],
"total": 1,
"page": 1,
"limit": 10,
"totalPages": 1,
"hasNext": false,
"hasPrev": false
}Advanced Example: Pagination and Filtering
GET /tours?category=adventure&page=2&limit=1&sortBy=price&sortOrder=desc
Sample Response:
{
"data": [],
"total": 1,
"page": 2,
"limit": 1,
"totalPages": 2,
"hasNext": false,
"hasPrev": true
}- Endpoint:
GET /tours/categories - Description: List all available tour categories.
- Authentication: π Public
Sample Response (200 OK):
["adventure", "cultural", "wildlife", "food"]- Endpoint:
GET /tours/:id - Description: Get details for a specific tour.
- Authentication: π Public
Sample Response (200 OK):
{
"id": "tour-uuid-123",
"title": "Amazing Bali Adventure",
"description": "Experience the beauty of Bali with our 7-day adventure tour including temples, beaches, and rice terraces.",
"location": "Bali, Indonesia",
"price": 1299.99,
"startDate": "2025-06-01T00:00:00.000Z",
"endDate": "2025-06-07T00:00:00.000Z",
"capacity": 20,
"imageUrl": "https://images.unsplash.com/photo-1537996194471-e657df975ab4",
"category": "adventure",
"guide": {
"id": "guide-uuid-123",
"name": "Tour Guide",
"email": "guide@tripmate.com"
},
"createdAt": "2024-07-20T10:00:00.000Z",
"updatedAt": "2024-07-20T10:00:00.000Z"
}Advanced Example: Not Found (404)
{
"statusCode": 404,
"message": "Tour with ID tour-uuid-999 not found",
"error": "Not Found"
}- Endpoint:
GET /tours/:id/availability - Description: Get available capacity for a tour.
- Authentication: π Public
Sample Response (200 OK):
{
"availableCapacity": 18
}- Endpoint:
GET /tours/statistics/overview - Description: Get statistics about tours (total, by category, average price, etc.).
- Authentication: π Admin only
Sample Response (200 OK):
{
"totalTours": 4,
"totalCategories": 4,
"averagePrice": 1574.99,
"upcomingTours": 3,
"ongoingTours": 1,
"completedTours": 0,
"toursByCategory": {
"adventure": 1,
"cultural": 1,
"wildlife": 1,
"food": 1
}
}- Endpoint:
PUT /tours/:id - Description: Update a tour. Only Admins/Guides can update.
- Authentication: π Protected (Admin/Guide)
Sample Request:
{
"price": 1399.99,
"capacity": 25
}Sample Response (200 OK):
{
"id": "tour-uuid-123",
"title": "Amazing Bali Adventure",
"description": "Experience the beauty of Bali with our 7-day adventure tour including temples, beaches, and rice terraces.",
"location": "Bali, Indonesia",
"price": 1399.99,
"startDate": "2025-06-01T00:00:00.000Z",
"endDate": "2025-06-07T00:00:00.000Z",
"capacity": 25,
"imageUrl": "https://images.unsplash.com/photo-1537996194471-e657df975ab4",
"category": "adventure",
"guide": {
"id": "guide-uuid-123",
"name": "Tour Guide",
"email": "guide@tripmate.com"
},
"createdAt": "2024-07-20T10:00:00.000Z",
"updatedAt": "2024-07-21T10:00:00.000Z"
}Advanced Example: Forbidden (403)
{
"statusCode": 403,
"message": "You do not have permission to update this tour",
"error": "Forbidden"
}- Endpoint:
DELETE /tours/:id - Description: Delete a tour. Only Admins/Guides can delete.
- Authentication: π Protected (Admin/Guide)
Sample Response (200 OK):
{}Advanced Example: Forbidden (403)
{
"statusCode": 403,
"message": "You do not have permission to delete this tour",
"error": "Forbidden"
}- Endpoint:
GET /tours/search/suggestions?q=... - Description: Get autocomplete suggestions for locations/titles.
- Authentication: π Public
Sample Response (200 OK):
{
"locations": ["Bali, Indonesia", "Paris, France"],
"titles": ["Amazing Bali Adventure", "Paris City Tour"]
}Manage tour bookings, status, and statistics.
- Endpoint:
POST /bookings - Description: Book a tour.
- Authentication: π Protected
- Request Body:
{ "tourId": "...", "specialRequests": "Vegetarian meals" } - Success Response (201 Created): Booking object.
- Endpoint:
GET /bookings - Description: List bookings. Admin sees all, users see their own.
- Authentication: π Protected
- Query Parameters:
status(enum)paymentStatus(enum)fromDate(date)toDate(date)page(number)limit(number)
Advanced Example: Filtering by Status and Pagination
GET /bookings?status=CONFIRMED&page=1&limit=2
Sample Response:
{
"data": [
{
"id": "booking-uuid-1",
"userId": "user-uuid-123",
"tourId": "tour-uuid-123",
"bookingDate": "2025-05-01T10:00:00.000Z",
"status": "CONFIRMED",
"paymentStatus": "PAID",
"amount": 1299.99,
"specialRequests": "Vegetarian meals please",
"createdAt": "2025-05-01T10:00:00.000Z",
"updatedAt": "2025-05-01T10:00:00.000Z"
}
],
"total": 1,
"page": 1,
"limit": 2,
"totalPages": 1
}- Endpoint:
GET /bookings/my-bookings - Description: List bookings for the current user.
- Authentication: π Protected
- Endpoint:
GET /bookings/statistics - Description: Get booking stats (total, confirmed, revenue, etc.).
- Authentication: π Protected
- Endpoint:
GET /bookings/:id - Description: Get details for a specific booking.
- Authentication: π Protected
Advanced Example: Forbidden (403)
{
"statusCode": 403,
"message": "You do not have permission to view this booking",
"error": "Forbidden"
}- Endpoint:
PUT /bookings/:id/status - Description: Update booking status (e.g., confirm, cancel).
- Authentication: π Admin only
- Request Body:
{ "status": "CONFIRMED" }
Advanced Example: Invalid Status Transition (400)
{
"statusCode": 400,
"message": "Cannot cancel a completed booking",
"error": "Bad Request"
}- Endpoint:
PUT /bookings/:id/cancel - Description: Cancel a booking.
- Authentication: οΏ½οΏ½ Protected
- Endpoint:
PUT /bookings/:id/confirm-payment - Description: Confirm payment for a booking.
- Authentication: π Admin only
Community posts for sharing experiences.
- Endpoint:
POST /posts - Description: Create a new post.
- Authentication: π Protected
- Request Body:
{ "title": "My Amazing Trip to Bali", "content": "Just returned from an incredible journey...", "imageUrl": "https://example.com/image.jpg" }
- Endpoint:
GET /posts - Description: List all posts (with search, pagination, sorting).
- Authentication: π Public
- Query Parameters:
search(string)userId(string)page(number)limit(number)sortBy(createdAt, likes, comments)sortOrder(asc|desc)
Advanced Example: Search and Sort
GET /posts?search=Bali&sortBy=likes&sortOrder=desc
Sample Response:
{
"data": [
{
"id": "post-uuid-1",
"userId": "user-uuid-123",
"title": "My Amazing Bali Adventure",
"content": "Just returned from an incredible 7-day journey through Bali...",
"imageUrl": "https://images.unsplash.com/photo-1559628233-100c798642d4",
"likes": 15,
"createdAt": "2025-05-01T10:00:00.000Z",
"updatedAt": "2025-05-01T10:00:00.000Z",
"author": {
"id": "user-uuid-123",
"name": "Test User",
"email": "user@tripmate.com",
"avatar": null
},
"commentCount": 2,
"isLikedByCurrentUser": true
}
],
"total": 1,
"page": 1,
"limit": 10,
"totalPages": 1,
"hasNext": false,
"hasPrev": false
}- Endpoint:
GET /posts/user/:userId - Description: List posts by a specific user.
- Authentication: π Public
- Endpoint:
GET /posts/:id - Description: Get details for a specific post.
- Authentication: π Public
Advanced Example: Not Found (404)
{
"statusCode": 404,
"message": "Post not found",
"error": "Not Found"
}- Endpoint:
PUT /posts/:id - Description: Update a post (author only).
- Authentication: π Protected
Advanced Example: Forbidden (403)
{
"statusCode": 403,
"message": "You can only update your own posts",
"error": "Forbidden"
}- Endpoint:
DELETE /posts/:id - Description: Delete a post (author or admin).
- Authentication: π Protected
Add and manage comments on posts.
- Endpoint:
POST /posts/:postId/comments - Description: Add a comment to a post.
- Authentication: π Protected
- Request Body:
{ "content": "Great post! Thanks for sharing." }
- Endpoint:
GET /posts/:postId/comments - Description: List comments for a post (with pagination).
- Authentication: π Public
- Query Parameters:
page(number)limit(number)
Advanced Example: Pagination
GET /posts/post-uuid-1/comments?page=2&limit=1
Sample Response:
{
"data": [],
"total": 2,
"page": 2,
"limit": 1,
"totalPages": 2
}- Endpoint:
DELETE /comments/:id - Description: Delete a comment (author or admin).
- Authentication: π Protected
Advanced Example: Forbidden (403)
{
"statusCode": 403,
"message": "You can only delete your own comments",
"error": "Forbidden"
}User notifications for bookings, posts, etc.
- Endpoint:
GET /notifications - Description: List notifications for the current user (with filters and pagination).
- Authentication: π Protected
- Query Parameters:
type(enum)isRead(boolean)page(number)limit(number)
Advanced Example: Filtering by Type and Pagination
GET /notifications?type=BOOKING_CONFIRMED&page=1&limit=1
Sample Response:
{
"data": [
{
"id": "notif-uuid-1",
"userId": "user-uuid-123",
"type": "BOOKING_CONFIRMED",
"title": "Booking Confirmed",
"content": "Your booking for Amazing Bali Adventure has been confirmed!",
"isRead": false,
"relatedId": "booking-uuid-1",
"createdAt": "2025-05-01T10:00:00.000Z"
}
],
"total": 1,
"page": 1,
"limit": 1,
"totalPages": 1,
"hasNext": false,
"hasPrev": false
}- Endpoint:
GET /notifications/stats - Description: Get notification statistics (total, unread, by type).
- Authentication: π Protected
- Endpoint:
GET /notifications/unread-count - Description: Get the count of unread notifications.
- Authentication: π Protected
- Endpoint:
GET /notifications/:id - Description: Get details for a specific notification.
- Authentication: π Protected
Advanced Example: Not Found (404)
{
"statusCode": 404,
"message": "Notification not found",
"error": "Not Found"
}- Endpoint:
PUT /notifications/mark-read - Description: Mark one or more notifications as read.
- Authentication: π Protected
- Request Body:
(If omitted, marks all as read)
{ "notificationIds": ["id1", "id2"] }
- Endpoint:
PUT /notifications/:id/unread - Description: Mark a notification as unread.
- Authentication: π Protected
- Endpoint:
DELETE /notifications/clear - Description: Delete all notifications for the user.
- Authentication: π Protected
- Endpoint:
DELETE /notifications/:id - Description: Delete a specific notification.
- Authentication: π Protected
- Test Database:
- The project uses a dedicated test Postgres database container for e2e tests, defined in
docker-compose.test.yml. - Test environment variables are stored in
.env.test(ignored in version control and Docker builds).
- The project uses a dedicated test Postgres database container for e2e tests, defined in
- Start the test database:
npm run test:e2e:db
- Run migrations and seed the test DB:
npx prisma migrate deploy --schema=prisma/schema.prisma npm run test:e2e:seed
- Run e2e tests:
(This script will start the DB, migrate, seed, run tests, and shut down the DB automatically.)
npm run test:e2e
test:e2e:db- Start the test DB containertest:e2e:db:down- Stop and remove the test DB containertest:e2e:seed- Seed the test DBtest:e2e- Full e2e test cycle (db up, migrate, seed, test, db down)
.env.test- Used for test DB credentials and secrets (ignored in git and Docker)
- The project uses GitHub Actions for CI. The workflow runs lint, unit, integration, and e2e tests, and builds the project on every push and pull request to
master/main. - See
.github/workflows/ci.ymlfor details.
-
Unit & Integration Tests:
npm test -
Watch Mode:
npm run test:watch
-
Coverage Report:
npm run test:cov # or for HTML report npm run test:cov:html -
End-to-End Tests:
npm run test:e2e
Coverage reports are generated in the coverage/ directory. The project aims for at least 80% coverage on all metrics (branches, functions, lines, statements).
Contributions are what make the open-source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.
If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement".
- Fork the Project
- Create your Feature Branch (
git checkout -b feature/AmazingFeature) - Commit your Changes (
git commit -m 'Add some AmazingFeature') - Push to the Branch (
git push origin feature/AmazingFeature) - Open a Pull Request
- Email Verification: OTP-based email verification for new users
- Password Hashing: Bcrypt with salt rounds
- Rate Limiting: OTP resend limited to 1 per minute
- JWT Security: Short-lived access tokens with refresh token rotation
- Input Validation: Comprehensive request validation
- SQL Injection Protection: Parameterized queries via Prisma
- CORS Protection: Configurable allowed origins
- Environment Variables: Sensitive data kept in env files
This project is distributed under the MIT License. See LICENSE for more information.
Happy Travels and Happy Coding!
The project uses a robust GitHub Actions workflow for Continuous Integration (CI):
- Triggers: On every push and pull request to
master/main. - Jobs:
- Checkout code
- Set up Node.js (uses Node 20)
- Start PostgreSQL service (runs a Postgres 15 container)
- Wait for DB health
- Install dependencies (
npm ci) - Run Prisma migrations (
npx prisma migrate deploy) - Seed the database (
npx prisma db seed) - Generate Prisma client (
npm run prisma:generate) - Lint code (
npm run lint) - Run unit & integration tests (
npm test) - Run e2e tests (
npm run test:e2e) - Build project (
npm run build) - Upload coverage report (as an artifact)
name: CI
on:
push:
branches: [ master, main ]
pull_request:
branches: [ master, main ]
jobs:
build-and-test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: mysecretpassword
POSTGRES_DB: trip_mate
ports:
- 5432:5432
options: >-
--health-cmd="pg_isready -U postgres" --health-interval=10s --health-timeout=5s --health-retries=5
env:
DATABASE_URL: postgresql://postgres:mysecretpassword@localhost:5432/trip_mate
JWT_SECRET: dummy_jwt_secret_for_ci
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Wait for PostgreSQL
run: |
for i in {1..30}; do
if pg_isready -h localhost -p 5432 -U postgres; then
echo "Postgres is ready!" && break
fi
echo "Waiting for Postgres..."
sleep 2
done
- name: Install dependencies
run: npm ci
- name: Run Prisma Migrations
run: npx prisma migrate deploy
- name: Seed Database
run: npx prisma db seed
- name: Generate Prisma client
run: npm run prisma:generate
- name: Lint code
run: npm run lint
- name: Run unit & integration tests
run: npm test
- name: Run e2e tests
run: npm run test:e2e
- name: Build project
run: npm run build
- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/You can simulate the CI workflow locally using the provided scripts:
- Lint:
npm run lint - Unit/Integration Tests:
npm test - E2E Tests:
npm run test:e2e(uses Docker for test DB) - Build:
npm run build - Coverage:
npm run test:covornpm run test:cov:html
Tip: Always run
npm run lintandnpm testbefore pushing to catch issues early.
- Never commit
.env,.env.test, or other secrets. - Keep
docker-compose.test.ymland.env.testout of Docker images and version control. - Use
npm ciin CI for clean, reproducible installs. - Keep your Prisma schema and migrations in sync.
- Seed your test DB for reliable e2e tests.
- Upload coverage reports as artifacts for review.
- Database connection errors: Ensure the test DB container is healthy and env vars are correct.
- Test failures: Check logs for stack traces. Run tests locally with the same scripts as CI.
- Lint errors: Run
npm run lint -- --fixto auto-fix common issues. - Out-of-date Prisma client: Run
npx prisma generateafter schema changes. - Docker issues: Make sure Docker is running and ports are not in use.
For more, see the .github/workflows/ci.yml file and the scripts in package.json.
