Implementing Bookings API Endpoints

Understanding the Problem

Based on the specifications, we need to implement several endpoints for the Bookings resource:

Each endpoint has specific requirements for authentication, authorization, validation, and response formatting. Additionally, we need to implement booking conflict validation to ensure users can't book spots that are already reserved for the requested dates.

Planning the Solution

  1. Set up database migration for the Bookings table
  2. Create Sequelize model with validations and associations
  3. Set up the bookings router and connect it to the main API router
  4. Implement each endpoint with proper validation and authorization
  5. Implement booking conflict validation
  6. Test each endpoint to ensure it works as expected

Step 1: Database Migration

First, let's create the database migration for the Bookings table.

Create Bookings Migration

Run the following command to generate a migration file:

npx sequelize model:generate --name Booking --attributes spotId:integer,userId:integer,startDate:date,endDate:date

Now, modify the generated migration file to add constraints:

// backend/db/migrations/XXXXXXXXXXXXXX-create-booking.js
// Pseudocode:
/*
1. Set up options object for production environment
2. Define up method:
   - Create Bookings table with these fields:
     - id: Primary key, auto-increment, non-nullable integer
     - spotId: Non-nullable integer, references Spots.id with cascade delete
     - userId: Non-nullable integer, references Users.id with cascade delete
     - startDate: Non-nullable date
     - endDate: Non-nullable date
     - createdAt: Non-nullable date with CURRENT_TIMESTAMP default
     - updatedAt: Non-nullable date with CURRENT_TIMESTAMP default
3. Define down method:
   - Drop Bookings table
*/

Now, run the migration to create the Bookings table:

npx dotenv sequelize db:migrate

Step 2: Create Sequelize Model

Next, let's update the generated model file to add validations and associations.

Update Booking Model

// backend/db/models/booking.js
// Pseudocode:
/*
1. Import required modules
2. Define Booking class extending Model
3. Define static associate method:
   - Booking belongs to User through userId
   - Booking belongs to Spot through spotId
4. Initialize Booking with these fields and validations:
   - spotId: Non-nullable integer
   - userId: Non-nullable integer
   - startDate: Non-nullable date with validations:
     - Must be a valid date
     - Cannot be on or after endDate
     - Cannot be in the past
   - endDate: Non-nullable date with validations:
     - Must be a valid date
     - Cannot be on or before startDate
5. Export Booking model
*/

Step 3: Set Up Bookings Router

Now, let's create a router file for the Bookings resource:

// backend/routes/api/bookings.js
// Pseudocode:
/*
1. Import required modules:
   - express
   - Sequelize models (Booking, Spot, User, SpotImage)
   - Authentication middleware (requireAuth)
   - Validation utilities (check, handleValidationErrors)
   - Sequelize operators (Op)

2. Create router object

3. Define validation middleware for bookings:
   - Validate startDate:
     - Must exist
     - Must be a valid date
     - Cannot be in the past
   - Validate endDate:
     - Must exist
     - Must be a valid date
     - Cannot be on or before startDate
   - Apply handleValidationErrors

4. Implement GET /api/bookings/current endpoint:
   - Apply requireAuth middleware
   - Get current user ID from request
   - Query database for bookings by this user
   - Include associated Spot with its preview image
   - Format response with proper data structure
   - Return JSON response with bookings array


5. Implement PUT /api/bookings/:bookingId endpoint:
   - Apply requireAuth and validateBooking middleware
   - Extract booking ID, startDate, endDate from request
   - Find the booking by ID
   - Check if booking exists, return 404 if not
   - Check if user owns the booking, return 403 if not
   - Check if booking has already passed, return 403 if so
   - Convert dates for comparison
   - Check for booking conflicts:
     - Case 1: Start date during another booking
     - Case 2: End date during another booking
     - Case 3: Another booking contained within new dates
   - If conflicts exist, return 403 with specific error messages
   - Update the booking with new dates
   - Return JSON response with updated booking

6. Implement DELETE /api/bookings/:bookingId endpoint:
   - Apply requireAuth middleware
   - Extract booking ID from request
   - Find the booking by ID with its spot
   - Check if booking exists, return 404 if not
   - Check if user owns the booking or the spot, return 403 if not
   - Check if booking has already started, return 403 if so
   - Delete the booking
   - Return success message
*/

Setting Up Spot-specific Booking Routes

Now, let's add routes to handle bookings for specific spots. We'll add these to the spots router:

// backend/routes/api/spots.js
// Pseudocode:
/*
1. Add required imports if not already present
   - Booking model
   - Validation middleware for bookings

2. Implement GET /api/spots/:spotId/bookings endpoint:
   - Apply requireAuth middleware
   - Extract spot ID and user ID
   - Check if spot exists, return 404 if not
   - If user is the owner:
     - Return detailed booking info including User
   - If user is NOT the owner:
     - Return limited booking info (spotId, startDate, endDate)
   - Return JSON response with bookings array

3. Implement POST /api/spots/:spotId/bookings endpoint:
   - Apply requireAuth and validateBooking middleware
   - Extract spot ID, user ID, startDate, endDate
   - Check if spot exists, return 404 if not
   - Check if user owns the spot, return 403 if so (cannot book own spot)
   - Convert dates for comparison
   - Check for booking conflicts with same logic as in PUT endpoint
   - If conflicts exist, return 403 with specific error messages
   - Create the new booking
   - Return 201 status with JSON response of created booking
*/

Connect the Bookings Router to the Main API Router

Now, let's update the backend/routes/api/index.js file to include our bookings router:

// backend/routes/api/index.js
// Pseudocode:
/*
1. Import all required router modules:
   - sessionRouter
   - usersRouter
   - spotsRouter
   - reviewsRouter
   - bookingsRouter
   - restoreUser middleware

2. Apply restoreUser middleware to set req.user

3. Connect all router modules:
   - router.use('/session', sessionRouter)
   - router.use('/users', usersRouter)
   - router.use('/spots', spotsRouter)
   - router.use('/reviews', reviewsRouter)
   - router.use('/bookings', bookingsRouter)

4. Export router
*/

Create Seed Data for Testing Bookings

Let's create some seed data to test our newly created booking endpoints. Generate a new seeder file:

npx sequelize seed:generate --name demo-bookings

Edit the generated file to create sample bookings:

// backend/db/seeders/XXXXXXXXXXXXXX-demo-bookings.js
// Pseudocode:
/*
1. Import required models (User, Spot, Booking)
2. Set up options object for production environment
3. Define up method:
   - Get demo users (Demo-lition, FakeUser1, FakeUser2)
   - Get all spots
   - Create helper function to calculate future dates
   - Create sample bookings with bulkCreate:
     - Booking 1: spot1, user1, days 10-15
     - Booking 2: spot1, user2, days 20-25
     - Booking 3: spot2, demoUser, days 5-8
     - Booking 4: spot2, user2, days 12-16
     - Booking 5: spot3, user1, days 30-35
4. Define down method:
   - Delete all records from Bookings table
*/

Run the seeder to populate the database:

npx dotenv sequelize db:seed:all

Testing Bookings Endpoints

Now that we have implemented all the endpoints for the Bookings resource, let's test them to ensure they work as expected.

Testing GET /api/bookings/current

First, make sure you're logged in, then run:

// Testing GET /api/bookings/current endpoint:
fetch('/api/bookings/current')
  .then(res => res.json())
  .then(data => console.log(data));

You should see the bookings created by the current user.

Testing GET /api/spots/:spotId/bookings

// Testing GET /api/spots/:spotId/bookings endpoint:
fetch('/api/spots/1/bookings')
  .then(res => res.json())
  .then(data => console.log(data));

You should see the bookings for spot 1. The response format will depend on whether you own the spot or not.

Testing POST /api/spots/:spotId/bookings

First, get a CSRF token, then create a booking for a spot:

// Testing POST /api/spots/:spotId/bookings endpoint:
// Pseudocode:
/*
1. Get CSRF token from /api/csrf/restore
2. Use token to create a booking for spot 3:
   - Set method to POST
   - Include headers with CSRF token
   - Include JSON body with startDate and endDate
3. Log the response
*/

You should see information about the newly created booking.

Testing PUT /api/bookings/:bookingId

Update a booking you created:

// Testing PUT /api/bookings/:bookingId endpoint:
// Pseudocode:
/*
1. Get CSRF token from /api/csrf/restore
2. Use token to update booking with ID 6:
   - Set method to PUT
   - Include headers with CSRF token
   - Include JSON body with updated startDate and endDate
3. Log the response
*/

You should see information about the updated booking.

Testing DELETE /api/bookings/:bookingId

Delete a booking you created:

// Testing DELETE /api/bookings/:bookingId endpoint:
// Pseudocode:
/*
1. Get CSRF token from /api/csrf/restore
2. Use token to delete booking with ID 6:
   - Set method to DELETE
   - Include headers with CSRF token
3. Log the response
*/

You should see a success message indicating the booking has been deleted.

Testing Booking Conflicts

Let's test the booking conflict validation by trying to create a booking that overlaps with an existing one:

// Testing booking conflict validation:
// Pseudocode:
/*
1. Get CSRF token from /api/csrf/restore
2. First get existing bookings for spot 1
3. Try to create a booking that conflicts with an existing one:
   - Set startDate during an existing booking period
   - Set endDate during an existing booking period
4. Log the error response
*/

You should get a 403 error with a message about the spot being already booked for the specified dates.

Note: You'll need to define the futureDate function for the above test:

// Helper function for date calculation
// Pseudocode:
/*
Define futureDate function that:
1. Takes number of days to add
2. Creates a new Date object
3. Adds the specified days
4. Returns formatted date string (YYYY-MM-DD)
*/

Implementing SpotImage Delete Endpoint

Finally, let's add the endpoint to delete spot images. Add this to the spots router:

// DELETE /api/spot-images/:imageId endpoint:
// Pseudocode:
/*
1. Create DELETE route for /spots/images/:imageId
2. Apply requireAuth middleware
3. Extract image ID from request
4. Find image by ID with its associated spot
5. Check if image exists, return 404 if not
6. Check if current user owns the spot, return 403 if not
7. Delete the image
8. Return success message
*/

Test this endpoint with:

// Testing DELETE /api/spots/images/:imageId endpoint:
// Pseudocode:
/*
1. Get CSRF token from /api/csrf/restore
2. Use token to delete spot image with ID 1:
   - Set method to DELETE
   - Include headers with CSRF token
3. Log the response
*/

Summary and Next Steps

Congratulations! We have now implemented all the API endpoints required for our Airbnb clone:

Each endpoint includes proper validation, authorization, and error handling to ensure a robust API. We've also created seed data for testing and verified that each endpoint works as expected.

Here are some potential next steps for enhancing the API:

  1. API Documentation: Create comprehensive documentation for your API using tools like Swagger or Postman.
  2. Rate Limiting: Implement rate limiting to prevent abuse of your API.
  3. Pagination: Enhance the existing pagination for the Spots endpoint to apply to other list endpoints.
  4. Search Functionality: Add more advanced search capabilities, such as searching by spot name or description.
  5. User Profiles: Enhance user profiles with additional information and avatar images.
  6. Favorites: Allow users to mark spots as favorites.
  7. Categories/Tags: Add the ability to categorize spots by type (apartment, house, etc.) or features (pool, wifi, etc.).
  8. Frontend Development: Begin building a user interface that consumes these API endpoints.

With this foundation, you now have a complete backend for an Airbnb-like application that you can continue to build upon.