Implementing Spots API Endpoints

Understanding the Problem

Looking at the provided specifications, we need to implement several endpoints for the Spots resource:

Each endpoint has specific requirements for authentication, authorization, request 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 Spots and SpotImages tables
  2. Create Sequelize models with validations and associations
  3. Set up the spots 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 Spots and SpotImages tables.

Create Spots Migration

Run the following command to generate a migration file:

npx sequelize model:generate --name Spot --attributes ownerId:integer,address:string,city:string,state:string,country:string,lat:decimal,lng:decimal,name:string,description:string,price:decimal

Now, modify the generated migration file to add constraints and default values. Here's the pseudocode for what this file should contain:

// backend/db/migrations/XXXXXXXXXXXXXX-create-spot.js
// Pseudocode for migration file
/*
1. Set up options object for production environment
2. Define up method:
   - Create Spots table with these fields:
     - id: Primary key, auto-increment, non-nullable integer
     - ownerId: Non-nullable integer, references Users.id with cascade delete
     - address: Non-nullable string
     - city: Non-nullable string
     - state: Non-nullable string
     - country: Non-nullable string
     - lat: Non-nullable decimal(10,7)
     - lng: Non-nullable decimal(10,7)
     - name: Non-nullable string(50)
     - description: Non-nullable text
     - price: Non-nullable decimal(10,2)
     - createdAt: Non-nullable date with CURRENT_TIMESTAMP default
     - updatedAt: Non-nullable date with CURRENT_TIMESTAMP default
3. Define down method:
   - Drop Spots table
*/

Create SpotImages Migration

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

npx sequelize model:generate --name SpotImage --attributes spotId:integer,url:string,preview:boolean

Modify the generated migration file. Here's the pseudocode:

// backend/db/migrations/XXXXXXXXXXXXXX-create-spot-image.js
// Pseudocode for migration file
/*
1. Set up options object for production environment
2. Define up method:
   - Create SpotImages table with these fields:
     - id: Primary key, auto-increment, non-nullable integer
     - spotId: Non-nullable integer, references Spots.id with cascade delete
     - url: Non-nullable string
     - preview: Non-nullable boolean with default false
     - createdAt: Non-nullable date with CURRENT_TIMESTAMP default
     - updatedAt: Non-nullable date with CURRENT_TIMESTAMP default
3. Define down method:
   - Drop SpotImages 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 Spot Model

// backend/db/models/spot.js
// Pseudocode for Spot model
/*
1. Import required modules
2. Define Spot class extending Model
3. Define static associate method:
   - Spot belongs to User through ownerId (as Owner)
   - Spot has many SpotImages through spotId
   - Spot has many Reviews through spotId
   - Spot has many Bookings through spotId
4. Initialize Spot with these fields and validations:
   - ownerId: Non-nullable integer
   - address: Non-nullable string with validation for non-empty
   - city: Non-nullable string with validation for non-empty
   - state: Non-nullable string with validation for non-empty
   - country: Non-nullable string with validation for non-empty
   - lat: Non-nullable decimal with validation for range (-90 to 90)
   - lng: Non-nullable decimal with validation for range (-180 to 180)
   - name: Non-nullable string(50) with validation for length
   - description: Non-nullable text with validation for non-empty
   - price: Non-nullable decimal with validation for positive value
5. Export Spot model
*/

Update SpotImage Model

// backend/db/models/spotimage.js
// Pseudocode for SpotImage model
/*
1. Import required modules
2. Define SpotImage class extending Model
3. Define static associate method:
   - SpotImage belongs to Spot through spotId
4. Initialize SpotImage with these fields and validations:
   - spotId: Non-nullable integer
   - url: Non-nullable string with validation for URL format
   - preview: Boolean with default false
5. Export SpotImage model
*/

Update User Model Associations

Add the following association to the User model:

// backend/db/models/user.js
// Pseudocode for updating User model associations
/*
Inside the User.associate method, add:
- User has many Spots through ownerId with cascade delete
- User has many Reviews through userId with cascade delete
- User has many Bookings through userId with cascade delete
*/

Step 3: Set Up Spots Router

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

// backend/routes/api/spots.js
// Pseudocode for Spots router

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

2. Create router object

3. Define validation middleware:
   - validateSpot: Validates all spot fields
   - validateQueryFilters: Validates query parameters for pagination and filtering

4. Implement GET /api/spots:
   - Apply validateQueryFilters middleware
   - Parse query parameters with defaults (page=1, size=20)
   - Build filtering conditions based on query parameters
   - Calculate pagination (limit, offset)
   - Query database for spots with filters, pagination, and calculations
   - Format response with proper data types
   - Return JSON response with spots, page, and size

5. Implement GET /api/spots/current:
   - Apply requireAuth middleware
   - Get current user ID from request
   - Query database for spots owned by current user
   - Format response with proper data types
   - Return JSON response with spots

6. Implement GET /api/spots/:id:
   - Extract spot ID from request parameters
   - Query database for spot with details
   - Check if spot exists, return 404 if not
   - Format response with proper data types
   - Return JSON response with spot details

7. Implement POST /api/spots:
   - Apply requireAuth and validateSpot middleware
   - Extract spot data from request body
   - Create new spot with current user as owner
   - Format response with proper data types
   - Return 201 status with JSON response

8. Implement POST /api/spots/:id/images:
   - Apply requireAuth middleware
   - Extract spot ID and image data
   - Check if spot exists, return 404 if not
   - Check if current user is owner, return 403 if not
   - Create new spot image
   - Return 201 status with JSON response

9. Implement PUT /api/spots/:id:
   - Apply requireAuth and validateSpot middleware
   - Extract spot ID and updated data
   - Check if spot exists, return 404 if not
   - Check if current user is owner, return 403 if not
   - Update spot with new data
   - Format response with proper data types
   - Return JSON response with updated spot

10. Implement DELETE /api/spots/:id:
    - Apply requireAuth middleware
    - Extract spot ID
    - Check if spot exists, return 404 if not
    - Check if current user is owner, return 403 if not
    - Delete spot
    - Return success message

11. Export router
*/

Continuing Spots Router Implementation

Let's continue with the detailed pseudocode for each endpoint in the Spots router.

POST /api/spots/:id/images - Add an Image to a Spot

// POST /api/spots/:id/images - Add an Image to a Spot
// Pseudocode:
/*
1. Create a POST route for /api/spots/:id/images
2. Apply requireAuth middleware
3. Extract from request:
   - spotId from params
   - url and preview from body
4. Try:
   - Find spot by ID
   - If spot doesn't exist, return 404 with error message
   - If current user is not the owner, return 403 with "Forbidden" message
   - Create new SpotImage with spotId, url, preview
   - Format response with id, url, preview
   - Return 201 status with formatted response
5. Catch:
   - Return 500 status with error message
*/

PUT /api/spots/:id - Edit a Spot

// PUT /api/spots/:id - Edit a Spot
// Pseudocode:
/*
1. Create a PUT route for /api/spots/:id
2. Apply requireAuth and validateSpot middleware
3. Extract from request:
   - spotId from params
   - address, city, state, country, lat, lng, name, description, price from body
4. Try:
   - Find spot by ID
   - If spot doesn't exist, return 404 with error message
   - If current user is not the owner, return 403 with "Forbidden" message
   - Update spot with new data
   - Format response with all spot fields and proper data types
   - Return formatted response
5. Catch:
   - If validation error, return 400 with validation error messages
   - Otherwise, return 500 with error message
*/

DELETE /api/spots/:id - Delete a Spot

// DELETE /api/spots/:id - Delete a Spot
// Pseudocode:
/*
1. Create a DELETE route for /api/spots/:id
2. Apply requireAuth middleware
3. Extract spotId from params
4. Try:
   - Find spot by ID
   - If spot doesn't exist, return 404 with error message
   - If current user is not the owner, return 403 with "Forbidden" message
   - Delete the spot
   - Return success message
5. Catch:
   - Return 500 with error message
*/

Complete the router by exporting it:

// Export the router
module.exports = router;

Connect the Spots Router to the Main API Router

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

// backend/routes/api/index.js
// Pseudocode:
/*
1. Import express and create router
2. Import route modules:
   - sessionRouter
   - usersRouter
   - spotsRouter
3. Import restoreUser middleware
4. Apply restoreUser middleware to set req.user
5. Connect route modules:
   - router.use('/session', sessionRouter)
   - router.use('/users', usersRouter)
   - router.use('/spots', spotsRouter)
6. Export router
*/

Create Seed Data for Testing

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

npx sequelize seed:generate --name demo-spots

Edit the generated file to create sample spots. Here's the pseudocode:

// backend/db/seeders/XXXXXXXXXXXXXX-demo-spots.js
// Pseudocode:
/*
1. Set up options object for production environment
2. Define up method:
   - Find demo user
   - Create sample spots:
     - Spot 1: App Academy in San Francisco
     - Spot 2: Sunny Retreat in Los Angeles
     - Spot 3: Mountain View in Denver
   - Create sample spot images:
     - Preview image for Spot 1
     - Non-preview image for Spot 1
     - Preview image for Spot 2
     - Preview image for Spot 3
3. Define down method:
   - Delete all records from Spots and SpotImages tables
4. Export the seeder
*/

Run the seeder to populate the database:

npx dotenv sequelize db:seed:all

Testing Spots Endpoints

Now that we have implemented all the endpoints for the Spots resource, let's test them to ensure they work as expected. You can use tools like Postman or cURL, but for simplicity, we'll use the browser's fetch API via the browser console.

Testing GET /api/spots

Open your browser and navigate to your application (e.g., http://localhost:8000). Open the developer console and run:

// Testing GET /api/spots
fetch('/api/spots')
  .then(response => response.json())
  .then(data => console.log(data));

You should see a list of spots in the console.

Testing GET /api/spots/current

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

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

You should see the spots owned by the current user.

Testing GET /api/spots/:id

Use an ID from one of the spots you retrieved earlier:

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

You should see detailed information about the spot.

Testing POST /api/spots

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

// Testing POST /api/spots
// Pseudocode:
/*
1. Get CSRF token:
   fetch('/api/csrf/restore')
     .then(response => response.json())
     .then(data => {
       const csrfToken = data['XSRF-Token'];
       
       // Use token to create a spot
       return fetch('/api/spots', {
         method: 'POST',
         headers: {
           'Content-Type': 'application/json',
           'XSRF-TOKEN': csrfToken
         },
         body: JSON.stringify({
           address: '123 Test Street',
           city: 'Testville',
           state: 'Test State',
           country: 'Test Country',
           lat: 40.7128,
           lng: -74.0060,
           name: 'Test Spot',
           description: 'A test spot',
           price: 100
         })
       });
     })
     .then(response => response.json())
     .then(data => console.log(data));
*/

You should see information about the newly created spot.

Testing POST /api/spots/:id/images

Use the ID of the spot you just created:

// Testing POST /api/spots/:id/images
// Pseudocode:
/*
1. Get CSRF token:
   fetch('/api/csrf/restore')
     .then(response => response.json())
     .then(data => {
       const csrfToken = data['XSRF-Token'];
       
       // Use token to add an image to the spot
       return fetch('/api/spots/4/images', {
         method: 'POST',
         headers: {
           'Content-Type': 'application/json',
           'XSRF-TOKEN': csrfToken
         },
         body: JSON.stringify({
           url: 'https://example.com/test-image.jpg',
           preview: true
         })
       });
     })
     .then(response => response.json())
     .then(data => console.log(data));
*/

You should see information about the newly added image.

Testing PUT /api/spots/:id

Update the spot you created:

// Testing PUT /api/spots/:id
// Pseudocode:
/*
1. Get CSRF token:
   fetch('/api/csrf/restore')
     .then(response => response.json())
     .then(data => {
       const csrfToken = data['XSRF-Token'];
       
       // Use token to update the spot
       return fetch('/api/spots/4', {
         method: 'PUT',
         headers: {
           'Content-Type': 'application/json',
           'XSRF-TOKEN': csrfToken
         },
         body: JSON.stringify({
           address: '123 Updated Street',
           city: 'Updated City',
           state: 'Updated State',
           country: 'Updated Country',
           lat: 40.7128,
           lng: -74.0060,
           name: 'Updated Spot',
           description: 'An updated spot',
           price: 150
         })
       });
     })
     .then(response => response.json())
     .then(data => console.log(data));
*/

You should see information about the updated spot.

Testing DELETE /api/spots/:id

Delete the spot you created:

// Testing DELETE /api/spots/:id
// Pseudocode:
/*
1. Get CSRF token:
   fetch('/api/csrf/restore')
     .then(response => response.json())
     .then(data => {
       const csrfToken = data['XSRF-Token'];
       
       // Use token to delete the spot
       return fetch('/api/spots/4', {
         method: 'DELETE',
         headers: {
           'XSRF-TOKEN': csrfToken
         }
       });
     })
     .then(response => response.json())
     .then(data => console.log(data));
*/

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

Debugging Common Issues

If you encounter any issues during testing, here are some common problems and solutions:

For more specific debugging, you can add console.log statements in your route handlers to log intermediate values and the error object.

Next Steps

Now that we've implemented the Spots endpoints, we can move on to implementing the Reviews and 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 Reviews endpoints following this pattern.