Understanding the Problem
Looking at the provided specifications, we need to implement several endpoints for the Spots resource:
- GET /api/spots - Get all spots
- GET /api/spots/current - Get all spots owned by the current user
- GET /api/spots/:id - Get details of a specific spot
- POST /api/spots - Create a new spot
- POST /api/spots/:id/images - Add an image to a spot
- PUT /api/spots/:id - Edit a spot
- DELETE /api/spots/:id - Delete a spot
- GET /api/spots with query filters
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
- Set up database migrations for Spots and SpotImages tables
- Create Sequelize models with validations and associations
- Set up the spots router and connect it to the main API router
- Implement each endpoint with proper validation and authorization
- 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:
- 401 Unauthorized: Make sure you're logged in before testing endpoints that require authentication.
- 403 Forbidden: Check if you're trying to modify or delete a spot that doesn't belong to you.
- 404 Not Found: Verify that the spot ID you're using exists in the database.
- 400 Bad Request: Check the validation errors in the response to see which fields are invalid.
- 500 Server Error: Check your server logs for more details about the error.
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:
- Create database migrations and models
- Set up associations between models
- Create router files with appropriate validation and authorization
- Implement each endpoint according to the specifications
- Test each endpoint to ensure it works as expected
In the next part, we'll implement the Reviews endpoints following this pattern.