A Reddit clone application using React and Redux. The application will allow users to view and search posts and comments provided by its own API (Node.js, Express and PostgreSQL). Please note that credit for any branding and the logo goes to Reddit.
** PLEASE NOTE There is a big constraint with using the Reddit API for this project. Reddit's Data API now requires developers to formally request non-commerical access (which requires a support ticket, not instant signup) before the developer is approved to use it.
As this project is designed entirely to be a learning/portfolio project I have decided to go an alternative route to complete this project and make it fully functional. After carrying out some research I decided to create my own databases and build a real full-stack app. This means my forms will need to do genuine authentication.
- Users can use the application on any device (desktop to mobile)
- Users can use the application on any modern browser
- Users can access your application at a URL
- Users see an initial view of the data when first visiting the app
- Users can search the data using terms
- Users can filter the data based on categories that are predefined
- Users are shown a detailed view (modal or new page/route) when they select an item
- Users are delighted with a cohesive design system
- Users are delighted with animations and transitions
- Users are able to leave an error state
- Sign up and log in with real accounts (passwords hashed with bcrypt, logins proven with a JWT that lasts 7 days)
- Staying logged in after a page refresh, until the token expires
- Logging out automatically the moment the token expires, with a "session expired" notice (the user stays on the page they were reading)
- A feed of posts loaded from the database, newest first, with loading and error states and a Try again button
- Search posts by heading, and filter them by category (both work together)
- "Most Visited" shows posts from every category, ordered by total likes and dislikes
- Create a post (logged in users only) with a heading, content and category
- Like and dislike posts - one reaction per user per post, saved on the server, with the user's own reaction highlighted
- Read the comments on a post in a modal, and add comments when logged in
- Logged out users who try to react or comment are sent to the login form
- Share a post: the Share button copies a link (e.g.
/post/12) that opens the post on its own page
- Create a PostgreSQL database called
reddit_app(e.g. in pgAdmin: right-click Databases → Create → Database...) - Create the tables: open the Query Tool on the
reddit_appdatabase aspostgresand run the contents ofbackend/db/schema.sql - Create a service account user for the app and give it access to the tables. Run the following SQL on the
reddit_appdatabase (after step 2, becauseGRANT ... ON ALL TABLESonly covers tables that already exist):
CREATE USER reddit_app_user WITH PASSWORD 'choose_a_strong_password';
GRANT CONNECT ON DATABASE reddit_app TO reddit_app_user;
GRANT USAGE ON SCHEMA public TO reddit_app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO reddit_app_user;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO reddit_app_user;
-- tables and sequences created by postgres in future get the same permissions automatically
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO reddit_app_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO reddit_app_user;- Create a
backend/.envfile (never commit this file - it is listed in.gitignore) using the service account user you just created inDATABASE_URL:
DATABASE_URL=postgresql://reddit_app_user:choose_a_strong_password@localhost:5432/reddit_app
JWT_SECRET=a_long_random_string
PORT=5000
- Start the backend in one terminal (leave it running):
cd backend
npm install
npm startYou should see Server running on port 5000
6. Start the frontend in a second terminal from the root folder:
npm install
npm start- (Optional) Fill the database with sample users, posts, reactions and comments. From the
backendfolder:
npm run seedThis creates the users gamer_gemma, tech_tom and foodie_fran (all with the demo password password123) and 12 posts across different categories, with likes, dislikes and comments. It's safe to run more than once - it removes its previous sample data first rather than duplicating it.
Frontend (React Testing Library + Jest) - from the root folder:
npm testBackend (Jest + Supertest against a real database) - the backend tests use a separate database so they never touch real data:
- Create a second database called
reddit_app_test, runbackend/db/schema.sqlon it aspostgres, then run the sameGRANTandALTER DEFAULT PRIVILEGESSQL from step 3 withreddit_appreplaced byreddit_app_testin theGRANT CONNECTline - Create
backend/.env.test(also gitignored) - the same as.envbut pointing at the test database:
DATABASE_URL=postgresql://reddit_app_user:choose_a_strong_password@localhost:5432/reddit_app_test
JWT_SECRET=any_long_random_string_just_for_tests
- From the
backendfolder:
npm testThe tests empty every table before each test, so backend/tests/helpers.js refuses to run at all unless the database name ends in _test.
All routes start with /api. "Token" means the request needs an Authorization: Bearer <token> header from signing up or logging in.
| Method | Route | Token | What it does |
|---|---|---|---|
GET |
/api/health |
No | Checks the server is running |
POST |
/api/signup |
No | Creates an account from { username, email, password } and returns { token, user } |
POST |
/api/login |
No | Checks { username, password } and returns { token, user } |
GET |
/api/posts |
Optional | Every post, newest first, with the author, like/dislike counts and (when a token is sent) the viewer's own userReaction |
GET |
/api/posts/:id |
Optional | One post in the same shape as the feed (used by shared links), or 404 if it doesn't exist |
POST |
/api/posts |
Yes | Creates a post from { postHeading, content, category } - the author is taken from the token |
POST |
/api/posts/:id/reactions |
Yes | Sends { type: 'like' | 'dislike' } - adds, removes or switches the user's reaction and returns { likes, dislikes, userReaction } |
GET |
/api/posts/:id/comments |
No | A post's comments, oldest first |
POST |
/api/posts/:id/comments |
Yes | Adds a comment from { content } - the commenter is taken from the token |
- React
- Redux
- Git/GitHub
- GitHub Projects
Reddit API**- Node.js
- Express
- SQL (PostgeSQL, MySQL)
- JWT tokens (as I already have log in state held in the front-end)
- bcrypt (password hashing)
- React Router
- React Testing Library
- Supertest (backend API tests)
- HTML
- CSS
- JavaScript
- Jest
- Selenium
- Command line and file navigation
Implemented error handling for:
- A bad login: an error is displayed when login fails due to incorrect credentials
- A validation error on sign up form: an error is displayed when sign up passwords do not match
- Like/Dislike count resets to 1 every time the button is clicked
- A user can add a like and a dislike at the same time
- clicking like then dislike removes the like and adds a dislike instead (and vice versa)
- A user cannot add multiple likes/dislikes
- a second click of the button removes the like/dislike
- these rules now live on the server (
POST /api/posts/:id/reactions) and are backed up by aUNIQUE(post_id, user_id)rule in the database, so they apply to everyone and survive a refresh
- Every post in the feed isn't displaying the same data
- The feed can't be loaded (backend not running, or a server error)
- a "Could not load posts" message with a Try again button, rather than a misleading "no posts" message
- a "Loading posts..." message while waiting, so the page never looks empty or broken
- A logged out user tries to like, dislike or comment
- they're sent to the login form instead of the request failing
- A saved token has expired or isn't a real JWT when the page is refreshed
- the app starts logged out instead of showing "Log out" to someone the server would reject
- The token expires while the app is open
- a timer set for the token's
explogs the user out at that moment and shows a dismissible notice; logging out by hand cancels the timer so the notice never appears by mistake
- a timer set for the token's
- Creating a post or comment with empty fields
- caught in the browser before anything is sent, and checked again on the server (400)
- A post's category isn't one of the category buttons (backend)
- a 400 response, so every post can always be found with the category filter
- A request to a protected route with no token, a fake token or an expired token (backend)
- a 401 response from the
requireAuthmiddleware
- a 401 response from the
- Someone tries to post or comment as another user (backend)
- any
postedBysent in the request is ignored - the author always comes from the verified token
- any
- A reaction or comment on a post that doesn't exist (backend)
- a 404 "Post not found" response
- No posts/ a future API call returns nothing
- an intentional "no posts" message for the user so page doesn't look broken
- Missing fields when signing up or logging in (backend)
- a 400 response with a "... are required" message
- The email or username is already in use when signing up (backend)
- a 409 response with an "already in use" message
- A bad login (backend)
- a 401 response with the same "Incorrect username or password" message whether the username doesn't exist or the password is wrong, so an attacker can't work out which usernames exist
- Any other server error during sign up or log in (backend)
- a 500 response with a generic "Something went wrong" message; the real error is logged in the server terminal for debugging
App.test.js and auth.js are annotated. Please see the listed files for further details on each individual test.
The project is built test-first (TDD): each feature starts as failing tests, then the code is written to make them pass.
| Suite | Where | Tests | Covers |
|---|---|---|---|
| Frontend | src/App.test.js |
87 | Search, login/sign up/logout, staying logged in after a refresh, logging out when the token expires, loading the feed, creating posts, reactions, comments, the category filter, Most Visited, the shared post page |
| Backend | backend/tests/posts.test.js |
14 | Reading the feed and single posts, and creating posts (including auth and validation) |
| Backend | backend/tests/reactions.test.js |
15 | Adding, removing and switching reactions, and userReaction in the feed |
| Backend | backend/tests/comments.test.js |
12 | Reading and adding comments (including auth and validation) |
The frontend tests replace fetch with a Jest mock so they never need the backend running. The backend tests send real HTTP requests to the Express app with Supertest and run real SQL against the reddit_app_test database.
- Started the server using
node server.jsin one terminal and left it running, then sent requests from a second terminal - Checked the server was reachable:
Invoke-RestMethod http://localhost:5000/api/health- Tested sign up by running the below command in Powershell:
$body = @{ username = 'testuser'; email = 'test@example.com'; password = 'Secret123!' } | ConvertTo-Json
Invoke-RestMethod http://localhost:5000/api/signup -Method Post -ContentType 'application/json' -Body $body- Got back a token and a user object
- Tested login by running the below command in Powershell:
$body = @{ username = 'testuser'; password = 'Secret123!' } | ConvertTo-Json
Invoke-RestMethod http://localhost:5000/api/login -Method Post -ContentType 'application/json' -Body $body- Got back a new token and the user's details (without
password_hash) - Tested the error cases using a helper function so the status code and error message are printed instead of a red PowerShell exception:
function Test-Api($path, $data) {
try {
Invoke-RestMethod "http://localhost:5000/api/$path" -Method Post -ContentType 'application/json' -Body ($data | ConvertTo-Json)
} catch {
"$($_.Exception.Response.StatusCode.value__) $($_.ErrorDetails.Message)"
}
}| Test | Result |
|---|---|
| Sign up again with the same username and email | 409 "Username or email already in use" |
| Sign up with only a username | 400 "Username, email and password are required" |
| Log in with the wrong password | 401 "Incorrect username or password" |
| Log in with a username that doesn't exist | 401 with the same message as the wrong password |
| Log in without a password | 400 "Username and password are required" |
- Finally, checked in pgAdmin's Query Tool, ran
SELECT id, username, email, password_hash FROM users;and checked thatpassword_hashis a long string starting with$2b$10$(bcrypt with a cost factor of 10) and NOT the plain password
relation "users" does not exist(500 "Something went wrong"): the tables hadn't been created yet. Fixed by runningbackend/db/schema.sqlin pgAdmin's Query Tool on thereddit_appdatabasepermission denied for table users(500 "Something went wrong"): the tables were created aspostgres, soreddit_app_user(the user inDATABASE_URL) had no access to them. Fixed by running the table and sequenceGRANTs from How to Use, plus default privileges so tables created in future get the same permissions automatically:
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO reddit_app_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO reddit_app_user;- The frontend test suite wouldn't load at all (
Cannot find module 'react-router/dom', thenTextEncoder is not defined): Create React App's Jest doesn't understand React Router v7's package setup. Fixed with amoduleNameMapperentry inpackage.jsonand aTextEncoderpolyfill insrc/setupTests.js. Once the suite could run, it revealed 8 older tests that had been silently broken, which were then fixed .env.testaccidentally pointed at the real database: caught by the safety check inbackend/tests/helpers.jsbefore any data was deletedProxy error: Could not proxy request /index.css: a leftover<link href="index.css">inpublic/index.html- the styles are already bundled throughsrc/index.js, so the line was removed- A test that passed by accident: a typo (
setsPostsError) threw an error that thecatchblock turned into the expected message. Spotted by reading the code, not the test result - a reminder that atry/catchcatches your own mistakes too
- When deploying, configure the host to serve
index.htmlfor every page address ("SPA fallback") so shared/post/:idlinks load the app instead of a 404 - Get a custom domain name and use it for your application
- Set up a CI/CD workflow to automatically deploy your application when the master branch in the repository changes
- Make the application a progressive web app
