Skip to content

Latest commit

Β 

History

39 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Trip-Mate Backend ✈️

Trip-Mate Logo

The definitive backend solution for collaborative trip planning.
Built with NestJS, Prisma, and PostgreSQL.

NestJS Prisma PostgreSQL TypeScript

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.


πŸ“‹ Table of Contents


✨ Core Features

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

πŸ› οΈ Tech Stack


πŸ“ Project Structure

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.

πŸš€ Getting Started

Follow these steps to get a local copy of the project up and running.

Prerequisites

Installation & Setup

  1. Clone the Repository

    git clone https://github.com/KBLReddy/trip-mate-backend.git
    cd trip-mate-backend
  2. Install Dependencies

    npm install
  3. Set Up Environment Variables

    cp .env.example .env

    Edit .env with 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_NAME in your DATABASE_URL.

  4. Apply Database Migrations This command reads your prisma/schema.prisma file 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.

Running the Application

  • 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

πŸ” API Authentication Flow

The API uses JWT-based authentication with email verification:

Registration Flow

  1. User registers with email, password, and name
  2. System sends OTP (6-digit code) to user's email
  3. User verifies OTP within 10 minutes
  4. System returns JWT tokens (access & refresh tokens)

Authentication Headers

For protected endpoints, include the access token:

Authorization: Bearer <access_token>

Email Verification Process

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
Loading

πŸ“š API Endpoint Documentation

Note:

  • All endpoints marked as πŸ”’ Protected require you to include your access token in the Authorization header of every request:
    Authorization: Bearer <access_token>
    
  • You receive the access_token (and refresh_token) after logging in or registering.
  • Refresh tokens are only used for /auth/refresh and /auth/logout endpoints. For those, use:
    Authorization: Bearer <refresh_token>
    
    and include the refresh token in the request body as well.
  • If you omit the access token for protected endpoints, you will receive a 401 Unauthorized error.

Auth Module (/auth)

1. Register New User

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

2. Verify OTP

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

3. Resend OTP

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

4. Login

  • 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.

5. Refresh Access Token

  • Endpoint: POST /auth/refresh
  • Description: Refreshes the access token using a valid refresh token.
  • Authentication: πŸ”’ Requires valid refresh token in Authorization header.
  • 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.

6. Logout

  • Endpoint: POST /auth/logout
  • Description: Logs out the user and invalidates the refresh token.
  • Authentication: πŸ”’ Requires valid access token in Authorization header.
  • Request Body: application/json
    {
      "refreshToken": "<your_refresh_token>"
    }
  • Headers:
    Authorization: Bearer <access_token>
    
    and in the body:
    {
      "refreshToken": "<refresh_token>"
    }
  • Success Response (200 OK): Empty response.

JWT & Refresh Token Theory and Usage

  • JWT (JSON Web Token) is used for stateless authentication. After login, the server issues an accessToken (short-lived) and a refreshToken (longer-lived).
  • Access Token: Used in the Authorization header 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') })
});

Android (Kotlin + Jetpack Compose + Retrofit) Example

// 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) { /* ... */ }

Users Module (/users)

Handles user-specific data.

1. Get Current User Profile

  • 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.

2. Update User Profile

  • 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.

3. Change Password

  • 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.

4. Get All Users (Admin Only)

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

5. Get User by ID (Admin Only)

  • 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.

6. Update User by ID (Admin Only)

  • 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.

7. Delete User by ID (Admin Only)

  • 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.

Trips Module (/trips)

The core module for managing trips and all related data.

1. Create a New Trip

  • 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 & endDate must 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"
}

2. Get All User's Trips

  • 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

[]

3. Get a Single Trip by ID

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

4. Update a Trip

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

5. Delete a Trip

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

6. Add a Member to a Trip

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

7. Remove a Member from a Trip

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

Destinations Sub-Module (/trips/:id/destinations)

Manage the places you'll visit on your trip.

8. Add a Destination to a 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"
    }

9. Update a Destination

  • 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.

10. Delete a Destination

  • 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.

Activities Sub-Module (/trips/.../activities)

Manage the activities you'll do at each destination.

11. Add an Activity to a 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"
    }

12. Update an Activity

  • 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.

13. Delete an Activity

  • 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.

Expenses Sub-Module (/trips/:id/expenses)

Manage and track shared expenses for a trip.

14. Add an Expense to 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"
    }
    • category must 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"
    }

15. Update an Expense

  • 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.

16. Delete an 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.

Tours Module (/tours)

Manage tours, search, statistics, and categories.

1. Create a New Tour

  • 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.

2. Get All Tours

  • Endpoint: GET /tours
  • Description: List all tours with filters and pagination.
  • Authentication: πŸ”“ Public
  • Query Parameters:
    • search (string): Search by title/location
    • category (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
}

3. Get Tour Categories

  • Endpoint: GET /tours/categories
  • Description: List all available tour categories.
  • Authentication: πŸ”“ Public

Sample Response (200 OK):

["adventure", "cultural", "wildlife", "food"]

4. Get Tour Details

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

5. Get Tour Availability

  • Endpoint: GET /tours/:id/availability
  • Description: Get available capacity for a tour.
  • Authentication: πŸ”“ Public

Sample Response (200 OK):

{
  "availableCapacity": 18
}

6. Get Tour Statistics (Admin Only)

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

7. Update a Tour

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

8. Delete a Tour

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

9. Search Suggestions

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

Bookings Module (/bookings)

Manage tour bookings, status, and statistics.

1. Create a Booking

  • Endpoint: POST /bookings
  • Description: Book a tour.
  • Authentication: πŸ”’ Protected
  • Request Body:
    {
      "tourId": "...",
      "specialRequests": "Vegetarian meals"
    }
  • Success Response (201 Created): Booking object.

2. List Bookings

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

3. Get My Bookings

  • Endpoint: GET /bookings/my-bookings
  • Description: List bookings for the current user.
  • Authentication: πŸ”’ Protected

4. Get Booking Statistics

  • Endpoint: GET /bookings/statistics
  • Description: Get booking stats (total, confirmed, revenue, etc.).
  • Authentication: πŸ”’ Protected

5. Get Booking Details

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

6. Update Booking Status (Admin Only)

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

7. Cancel a Booking

  • Endpoint: PUT /bookings/:id/cancel
  • Description: Cancel a booking.
  • Authentication: οΏ½οΏ½ Protected

8. Confirm Payment (Admin Only)

  • Endpoint: PUT /bookings/:id/confirm-payment
  • Description: Confirm payment for a booking.
  • Authentication: πŸ”’ Admin only

Posts Module (/posts)

Community posts for sharing experiences.

1. Create a Post

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

2. List Posts

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

3. Get Posts by User

  • Endpoint: GET /posts/user/:userId
  • Description: List posts by a specific user.
  • Authentication: πŸ”“ Public

4. Get Post Details

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

5. Update a Post

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

6. Delete a Post

  • Endpoint: DELETE /posts/:id
  • Description: Delete a post (author or admin).
  • Authentication: πŸ”’ Protected

Comments Module (/posts/:postId/comments)

Add and manage comments on posts.

1. Add a Comment

  • Endpoint: POST /posts/:postId/comments
  • Description: Add a comment to a post.
  • Authentication: πŸ”’ Protected
  • Request Body:
    {
      "content": "Great post! Thanks for sharing."
    }

2. List Comments

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

3. Delete a Comment

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

Notifications Module (/notifications)

User notifications for bookings, posts, etc.

1. List Notifications

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

2. Get Notification Stats

  • Endpoint: GET /notifications/stats
  • Description: Get notification statistics (total, unread, by type).
  • Authentication: πŸ”’ Protected

3. Get Unread Count

  • Endpoint: GET /notifications/unread-count
  • Description: Get the count of unread notifications.
  • Authentication: πŸ”’ Protected

4. Get Notification Details

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

5. Mark Notifications as Read

  • Endpoint: PUT /notifications/mark-read
  • Description: Mark one or more notifications as read.
  • Authentication: πŸ”’ Protected
  • Request Body:
    {
      "notificationIds": ["id1", "id2"]
    }
    (If omitted, marks all as read)

6. Mark Notification as Unread

  • Endpoint: PUT /notifications/:id/unread
  • Description: Mark a notification as unread.
  • Authentication: πŸ”’ Protected

7. Clear All Notifications

  • Endpoint: DELETE /notifications/clear
  • Description: Delete all notifications for the user.
  • Authentication: πŸ”’ Protected

8. Delete a Notification

  • Endpoint: DELETE /notifications/:id
  • Description: Delete a specific notification.
  • Authentication: πŸ”’ Protected

πŸ§ͺ Testing & CI/CD

End-to-End (e2e) Test Environment

  • 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).

Running e2e Tests

  1. Start the test database:
    npm run test:e2e:db
  2. Run migrations and seed the test DB:
    npx prisma migrate deploy --schema=prisma/schema.prisma
    npm run test:e2e:seed
  3. Run e2e tests:
    npm run test:e2e
    (This script will start the DB, migrate, seed, run tests, and shut down the DB automatically.)

Test Scripts

  • test:e2e:db - Start the test DB container
  • test:e2e:db:down - Stop and remove the test DB container
  • test:e2e:seed - Seed the test DB
  • test:e2e - Full e2e test cycle (db up, migrate, seed, test, db down)

Environment Files

  • .env.test - Used for test DB credentials and secrets (ignored in git and Docker)

Continuous Integration (CI)

  • 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.yml for details.

πŸ§ͺ Running Tests

  • 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).


🀝 Contributing

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

  1. Fork the Project
  2. Create your Feature Branch (git checkout -b feature/AmazingFeature)
  3. Commit your Changes (git commit -m 'Add some AmazingFeature')
  4. Push to the Branch (git push origin feature/AmazingFeature)
  5. Open a Pull Request

πŸ”’ Security Features

  • 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

πŸ“œ License

This project is distributed under the MIT License. See LICENSE for more information.


Happy Travels and Happy Coding!


πŸ› οΈ CI/CD Pipeline Details

GitHub Actions Workflow

The project uses a robust GitHub Actions workflow for Continuous Integration (CI):

  • Triggers: On every push and pull request to master/main.
  • Jobs:
    1. Checkout code
    2. Set up Node.js (uses Node 20)
    3. Start PostgreSQL service (runs a Postgres 15 container)
    4. Wait for DB health
    5. Install dependencies (npm ci)
    6. Run Prisma migrations (npx prisma migrate deploy)
    7. Seed the database (npx prisma db seed)
    8. Generate Prisma client (npm run prisma:generate)
    9. Lint code (npm run lint)
    10. Run unit & integration tests (npm test)
    11. Run e2e tests (npm run test:e2e)
    12. Build project (npm run build)
    13. Upload coverage report (as an artifact)

Example Workflow File: .github/workflows/ci.yml

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/

Running CI Steps Locally

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:cov or npm run test:cov:html

Tip: Always run npm run lint and npm test before pushing to catch issues early.

Best Practices for CI/CD

  • Never commit .env, .env.test, or other secrets.
  • Keep docker-compose.test.yml and .env.test out of Docker images and version control.
  • Use npm ci in 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.

Troubleshooting CI/CD

  • 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 -- --fix to auto-fix common issues.
  • Out-of-date Prisma client: Run npx prisma generate after 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.


About

RESTful API for a travel and tour platform, built with NestJS, Prisma, and PostgreSQL. Features authentication, bookings, posts, comments, and notifications. Scalable, modular, and production-ready.

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages