Shredders
Shredders is a PERN web application designed to help snowboarders plan trips and see which of their friends have overlapping trips. It was created as an exercise for using PostgreSQL with React/Express and to provide a fun tool for snowboarders.
The idea came about last snowboard season when I ran into some friends by chance on the mountain and thought it'd be nice if there was a way to know ahead of time which friends had overlapping trips with you!
Table of Contents
- Features
- Demo
- API Reference
- Tech Stack
- Testing
- Continuous Integration/Continuous Deployment (CI/CD)
- Redis Caching
- Obstacles
- FAQ
- Features to Add
- Acknowledgements
Features
- User authentication (login and sign up)
- Search users and add friends
- Create trips
- Invite friends to trips (searchable)
- View all upcoming and past trips on the home page
- View RSVPs and headcount for each trip
- See which friends have overlapping trips with you
- Add/delete comments to trip pages to discuss and plan
- Upload avatars
- Responsive design for mobile and desktop screens
- Client side routing
- Form validation (client-side and Express)
Demo
https://shredders.onrender.com
NOTE: Render spins down a Free web service that goes 15 minutes without receiving inbound traffic. Render spins the service back up whenever it next receives a request to process. Spinning up a service takes up to a minute, which causes a noticeable delay for incoming requests until the service is back up and running. For example, a browser page load will hang temporarily.
API Reference
Documentation describing endpoints
Database schema

Tech Stack
| Category | Technology | Why |
|---|---|---|
| Frontend | React | Standard library for reusable UI components |
| Frontend | Vite | Build tool optimized for React with minimal configuration needed |
| Frontend | Tailwind CSS | Allows applying styles directly in the React components, reducing the need for custom classes |
| Frontend | React Router | Client side routing to reduce server load and seamless transitions between pages |
| Backend | Node.js/Express | Keeps coding language consistent between frontend and backend |
| Backend | PostgreSQL | Supports complex queries that were needed to fetch structured, related data |
| Backend | JWT | • Compact and efficient way to send authentication data between client and server • Contains all relevant info in payload about authenticated user |
| Backend | Redis | • Reduce load on PostgreSQL server • Improve API response times |
| CI/CD | Docker | • Packages application into containers, making it lightweight and portable to share and/or deploy • Can spin up development environment for frontend, backend, and db with one command using Docker Compose |
| CI/CD | GitHub Actions | Streamline test, build, and deploy process and reduce manual touchpoints |
| Web app hosting | Render | • Good free tier for hosting both client and server • Quick to spin up applications |
| File storage and DB hosting | Supabase | • Easy to use interface and good free tier option for storage and database • Database can be easily configured with their table/SQL editor |
Testing
The application includes comprehensive test coverage for frontend components using Jest and React Testing Library. Tests are located in the client/tests directory.
Test Coverage
The following components are currently tested:
- LoginForm: Tests form rendering, successful login navigation, user state updates, and error handling
- Avatar: Tests image rendering, edit functionality, and file upload behavior
- SearchableDropdown: Tests dropdown functionality and search behavior
- Trip: Tests that trip details renders correctly
- Plan: Tests trip creation functionality
- PostComment: Tests comment creation
- Utils: Tests utility functions
Running Tests
To run the tests:
- Navigate to the client directory:
cd client
- Run the tests:
npm run test
The test report will include the test coverage.
Continuous Integration/Continuous Deployment (CI/CD)
The project uses GitHub Actions for automated testing and deployment. The workflow is configured to:
- Run on every push to the main branch
- Install dependencies and run tests
- Build the project
- Deploy to Render using a webhook if tests and build pass
The workflow file is located at .github/workflows/render-deploy.yml.
Redis Caching
I wanted to learn how to use Redis, so I added caching to a few endpoints to improve API response time and reduce database load. I used Postman to test the response time with and without caching. Below is an example of one of the endpoints that was tested.
| Request | Total requests | Requests/s | Resp. time (Avg. ms) |
|---|---|---|---|
| GET Overview | 252 ▲ 8 | 1.97 ▲ 0.06 | 7,525 ▼ 412 |
A 412 ms decrease is observed for the average response time, which is about a 5% improvement.
Parameters for the test:
- 25 to 50 virtual users ramp up
- 2 minutes
Practically, given that the size of my database is only a few rows, the queries aren't that slow so I would consider caching to be over-engineering. But this was more for learning purposes.
Obstacles
Issues with Deployment
- CORS Configuration:
- Problem: Requests from the deployed frontend weren't going through, despite working in Postman and local testing.
- Solution: The CORS origin on the server was not set to allow the deployed client. Updated the server to set the origin based on an environment variable.
- Client-Side Routing:
- Problem: Receiving 404 errors when the client was routed to any subroutes.
- Solution: Redirect all routes to the
index.htmlbuild file. Render has a feature to handle this: Using Client-Side Routing.
- Hero Image Not Displaying:
- Problem: Hero image was not showing up; it received an inline style of
background-image: none. - Solution: Could not find any CSS or inline styles causing this. Got around the issue by using an
imgelement instead of adiv. Unsure about root cause.
- Problem: Hero image was not showing up; it received an inline style of
Backend and Frontend Integration
- Form Submission Issue:
- Problem: Getting a
TypeError: Failed to fetchdue to page refreshing before the response could be sent back. - Solution: Disabled the default form submission behavior to prevent the page from refreshing.
- Problem: Getting a
- Dropdown Options not Populating
- Problem: The invite friends dropdown on the plan trip page didn't show options at first because the component rendered before fetching data.
- Solution: Used a custom
useLoadhook, passing in the API call to ensure the component didn't render until the API call returned a response.
- Date Inconsistency:
- Problem: The dates entered in the form were consistent with what was uploaded to the database but displayed off by 1 day on the client. This issue was not present on the local app. It was determined that while the trip dates were being stored as a date type in the SQL database, running
pool.queryon the server converts them to a JavaScript date object with a UTC timezone like2024-04-07T00:00:00.000Z. However, when formatting this date on the client using theDate.getDate()method, the day of the local timezone was returned. For example, the date2024-04-07would be returned from the query as2024-04-07T00:00:00.000Z, andgetDate()would return 6 in local PDT time instead of 7. - Solution: Converted the query result back to a string with the
YYYY-MM-DDdate format instead of using the Date methods.
- Problem: The dates entered in the form were consistent with what was uploaded to the database but displayed off by 1 day on the client. This issue was not present on the local app. It was determined that while the trip dates were being stored as a date type in the SQL database, running
Database Queries
- Combining User and Friend Data:
- Problem: In the 'Add Friends' sidebar, needed a way to fetch all users and indicate if they were friends or if a friend request was pending.
- Solution: Used a
CASEclause in SQL. Left-joined theusertable with thefriendsandfriend requeststables, setting a status (e.g. pending) based on the joined row.
Frontend Behavior
- Comment Deletion:
- Problem: Deleting a comment other than the last one expanded the adjacent comment’s dropdown menu. The dropdown for any specific comment should only expand when clicked.
- Solution: This behavior was due to event propogation, where clicking a dropdown menu item triggered the dropdown icon's click handler. Fixed by disabling propogation.
- Comment Edit Icon Not Shown:
- Problem: Edit icon for comments was not showing up after user initially logged in
- Solution: The user object keys between login response and
useAuthhook were inconsistent. Login response would returnidkey whileuseAuthhook returneduser_idkey. Updated the backend logic for the login response to returnuser_idinstead ofid.
- Dropdown Menu for Inviting Friends:
- Problem: Wanted the dropdown to persist while clicking the options but hide when clicking outside the dropdown. Using the
onblurhandler wouldn't work because clicking on the options counted as a blur and would hide the menu. - Solution: Created a reference to the dropdown container and a
handleBlurcallback that hides the dropdown only if the newly focused element is eithernull(non-focusable) or not a child of the dropdown container. This seems to be a common design pattern in React.
- Problem: Wanted the dropdown to persist while clicking the options but hide when clicking outside the dropdown. Using the
Authentication and Authorization
- useAuth Hook and JWT:
- Problem: Server was sending JWT token as a cookie, but cookie would not persist on the client
- Solution: Modified CORS config to specify the client domain and included credentials with requests.
- Private Routes and Refresh:
- Problem: Refreshing a private route always redirected to the login page.
- Solution: This was because the
userstate variable in theuseAuthhook was reset to null upon refresh, and it takes time for the client to call the backend and update it. Fixed by adding aloadingstate variable and used it to only render the private route once the fetch call completed.
FAQ
What is the purpose of the useAuth hook in the client?
Provides a modular way to protect routes in the application. By wrapping the application with an authentication context, it allows us to check if the user is authenticated anywhere in the React app. If the user is not authenticated, the protected route can redirect them to the login page; otherwise, it will render the child components. The hook also returns data about the authenticated user, such as the user's name and avatar URL.
What is the purpose of the useLoad hook?
Accepts a callback and manages the isLoading state of asynchronous data fetching. Initializes isLoading to true and sets it to false once the callback completes. The state variable can then be used to display a loading spinner.
Features to Add
- Add button to edit RSVP to trip
- Cancel sent friend request
- Edit comments
- Edit trip (i.e. destination, dates)
Acknowledgements
- Images downloaded from Unsplash
- Icons from Font Awesome

Log in or sign up for Devpost to join the conversation.