Implementing Reviews API Endpoints

Understanding the Problem

From the provided specifications, we need to implement several endpoints for the Reviews resource:

Similar to the Spots resource, each endpoint has specific requirements for authentication, authorization, validation, and response formatting. We need to implement these endpoints while ensuring they meet these requirements.

Planning the Solution

  1. Set up database migrations for Reviews and ReviewImages tables
  2. Create Sequelize models with validations and associations
  3. Set up the reviews router and connect it to the main API router
  4. Implement each endpoint with proper validation and authorization
  5. Test each endpoint to ensure it works as expected

Step 1: Database Migrations

First, let's create the database migrations for the Reviews and ReviewImages tables.

Create Reviews Migration

Run the following command to generate a migration file:

npx sequelize model:generate --name Review --attributes spotId:integer,userId:integer,review:text,stars:integer

Now, modify the generated migration file to add constraints and default values:

// backend/db/migrations/XXXXXXXXXXXXXX-create-review.js
// Pseudocode:
/*
1. Set up options object for production environment
2. Define up method:
   - Create Reviews 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
     - review: Non-nullable text
     - stars: Non-nullable integer with validation (1-5)
     - createdAt: Non-nullable date with CURRENT_TIMESTAMP default
     - updatedAt: Non-nullable date with CURRENT_TIMESTAMP default
3. Define down method:
   - Drop Reviews table
*/

Create ReviewImages Migration

Run the following command to generate a migration file for ReviewImages:

npx sequelize model:generate --name ReviewImage --attributes reviewId:integer,url:string

Modify the generated migration file:

// backend/db/migrations/XXXXXXXXXXXXXX-create-review-image.js
// Pseudocode:
/*
1. Set up options object for production environment
2. Define up method:
   - Create ReviewImages table with these fields:
     - id: Primary key, auto-increment, non-nullable integer
     - reviewId: Non-nullable integer, references Reviews.id with cascade delete
     - url: Non-nullable string
     - createdAt: Non-nullable date with CURRENT_TIMESTAMP default
     - updatedAt: Non-nullable date with CURRENT_TIMESTAMP default
3. Define down method:
   - Drop ReviewImages table
*/

Now, run the migrations to create these tables:

npx dotenv sequelize db:migrate

Step 2: Create Sequelize Models

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

Update Review Model

// backend/db/models/review.js
// Pseudocode:
/*
1. Import required modules
2. Define Review class extending Model
3. Define static associate method:
   - Review belongs to User through userId
   - Review belongs to Spot through spotId
   - Review has many ReviewImages through reviewId with cascade delete
4. Initialize Review with these fields and validations:
   - spotId: Non-nullable integer
   - userId: Non-nullable integer
   - review: Non-nullable text with validation (non-empty)
   - stars: Non-nullable integer with validations:
     - Must be between 1 and 5
     - Must be an integer
5. Export Review model
*/

Update ReviewImage Model

// backend/db/models/reviewimage.js
// Pseudocode:
/*
1. Import required modules
2. Define ReviewImage class extending Model
3. Define static associate method:
   - ReviewImage belongs to Review through reviewId
4. Initialize ReviewImage with these fields and validations:
   - reviewId: Non-nullable integer
   - url: Non-nullable string with validations:
     - Must be non-empty
     - Must be a valid URL
5. Export ReviewImage model
*/

Step 3: Set Up Reviews Router

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

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

2. Create router object

3. Define validation middleware for reviews:
   - Validate review text:
     - Must exist and not be empty
   - Validate stars:
     - Must exist
     - Must be an integer between 1 and 5
   - Apply handleValidationErrors

4. Implement GET /api/reviews/current endpoint:
   - Apply requireAuth middleware
   - Get current user ID from request
   - Query database for reviews by this user
   - Include associated User, Spot (with preview image), and ReviewImages
   - Format response with proper data structure:
     - Add previewImage to each Spot
     - Arrange the data in the required format
   - Return JSON response with reviews array

5. Implement POST /api/reviews/:reviewId/images endpoint:
   - Apply requireAuth middleware
   - Extract review ID and image URL from request
   - Check if review exists, return 404 if not
   - Check if user owns the review, return 403 if not
   - Check if review already has 10 images, return 403 if so
   - Create new ReviewImage with reviewId and url
   - Return 201 status with image ID and URL
*/

Continuing Reviews Router Implementation

Let's continue implementing the remaining endpoints for the Reviews router.

PUT /api/reviews/:reviewId - Edit a Review

// PUT /api/reviews/:reviewId - Edit a Review
// Pseudocode:
/*
1. Create PUT route for /reviews/:reviewId
2. Apply requireAuth and validateReview middleware
3. Extract review ID, review text, stars from request
4. Find review by ID
5. Check if review exists, return 404 if not
6. Check if user owns the review, return 403 if not
7. Update the review with new text and stars
8. Format response with all review fields
9. Return JSON response with updated review
10. Handle validation errors by returning 400 with error messages
*/

DELETE /api/reviews/:reviewId - Delete a Review

// DELETE /api/reviews/:reviewId - Delete a Review
// Pseudocode:
/*
1. Create DELETE route for /reviews/:reviewId
2. Apply requireAuth middleware
3. Extract review ID from request
4. Find review by ID
5. Check if review exists, return 404 if not
6. Check if user owns the review, return 403 if not
7. Delete the review
8. Return success message
*/

Setting Up Spot-specific Review Routes

Now, let's add routes to handle reviews 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:
   - Review and ReviewImage models
   - Validation middleware for reviews

2. Implement GET /api/spots/:spotId/reviews endpoint:
   - Extract spot ID from request
   - Check if spot exists, return 404 if not
   - Query database for all reviews for this spot
   - Include associated User and ReviewImages
   - Return JSON response with reviews array

3. Implement POST /api/spots/:spotId/reviews endpoint:
   - Apply requireAuth and validateReview middleware
   - Extract spot ID, user ID, review text, stars from request
   - Check if spot exists, return 404 if not
   - Check if user already has a review for this spot, return 500 if so
   - Create new Review with spotId, userId, review text, stars
   - Return 201 status with created review data
   - Handle validation errors by returning 400 with error messages
*/

Connect the Reviews Router to the Main API Router

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

// backend/routes/api/index.js
// Pseudocode:
/*
1. Import all required router modules:
   - sessionRouter
   - usersRouter
   - spotsRouter
   - reviewsRouter
   - 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)

4. Export router
*/

Create Seed Data for Testing Reviews

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

npx sequelize seed:generate --name demo-reviews

Edit the generated file to create sample reviews:

// backend/db/seeders/XXXXXXXXXXXXXX-demo-reviews.js
// Pseudocode:
/*
1. Import required models (User, Spot, Review, ReviewImage)
2. Set up options object for production environment
3. Define up method:
   - Get demo users (Demo-lition, FakeUser1, FakeUser2)
   - Get all spots
   - Create sample reviews with bulkCreate:
     - Review 1: spot1, user1, 5 stars
     - Review 2: spot1, user2, 4 stars
     - Review 3: spot2, demoUser, 5 stars
     - Review 4: spot2, user2, 3 stars
     - Review 5: spot3, user1, 5 stars
   - Add sample review images:
     - Image 1 for Review 1
     - Image 2 for Review 1
     - Image 3 for Review 3
4. Define down method:
   - Delete all records from Reviews and ReviewImages tables
*/

Run the seeder to populate the database:

npx dotenv sequelize db:seed:all

Testing Reviews Endpoints

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

Testing GET /api/spots/:spotId/reviews

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

You should see a list of reviews for spot 1.

Testing GET /api/reviews/current

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

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

You should see the reviews created by the current user.

Testing POST /api/spots/:spotId/reviews

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

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

You should see information about the newly created review.

Testing POST /api/reviews/:reviewId/images

Add an image to the review you just created:

// Testing POST /api/reviews/:reviewId/images
// Pseudocode:
/*
1. Get CSRF token from /api/csrf/restore
2. Use token to add an image to review ID 6:
   - Set method to POST
   - Include headers with CSRF token
   - Include JSON body with image URL
3. Log the response
*/

You should see information about the newly added image.

Testing PUT /api/reviews/:reviewId

Update the review you created:

// Testing PUT /api/reviews/:reviewId
// Pseudocode:
/*
1. Get CSRF token from /api/csrf/restore
2. Use token to update review ID 6:
   - Set method to PUT
   - Include headers with CSRF token
   - Include JSON body with updated review text and stars
3. Log the response
*/

You should see information about the updated review.

Testing DELETE /api/reviews/:reviewId

Delete the review you created:

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

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

Handling Edge Cases and Error Conditions

It's important to test various edge cases to ensure our API is robust. Here are some edge cases you should test:

Trying to review a spot that doesn't exist

// Testing review for non-existent spot
// Pseudocode:
/*
1. Get CSRF token from /api/csrf/restore
2. Use token to create a review for spot ID 999:
   - Set method to POST
   - Include headers with CSRF token
   - Include JSON body with review text and stars
3. Log the response error
*/

You should get a 404 error with a message that the spot couldn't be found.

Trying to review a spot you've already reviewed

// Testing duplicate review
// Pseudocode:
/*
1. Get CSRF token from /api/csrf/restore
2. Use token to create a review for a spot you've already reviewed:
   - Set method to POST
   - Include headers with CSRF token
   - Include JSON body with review text and stars
3. Log the response error
*/

You should get a 500 error with a message that the user already has a review for this spot.

Trying to add more than 10 images to a review

This would require creating a review and then adding 11 images to it in sequence. The 11th image should fail with a 403 error.

Implementing ReviewImage Delete Endpoint

Let's add one more endpoint to delete review images. Add this to the reviews router:

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

Test this endpoint with:

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

Next Steps

Now that we've implemented the Reviews endpoints, we can move on to implementing the Bookings resources. The pattern will be similar:

  1. Create database migrations and models
  2. Set up associations between models
  3. Create router files with appropriate validation and authorization
  4. Implement each endpoint according to the specifications
  5. Test each endpoint to ensure it works as expected

In the next part, we'll implement the Bookings endpoints following this pattern.