A full-stack social media web app built with Flask, SQLite, React, and a custom REST API. Features infinite scroll, optimistic UI updates, double-click to like, and live comment rendering, all without page reloads. Scaled to 3,000+ requests per hour across 50+ concurrent users.
This was not one project — it was three, each building on the last. The first version was a static site generator written in Python that took JSON data and Jinja2 templates as input and rendered a non-interactive Instagram clone as static HTML files. The second version made it dynamic: Flask served pages from a SQLite database, handling full CRUD for users, posts, likes, comments, and follows, with session-based auth and SHA-512 password hashing.
The third version, the final one, kept the server-side back end and layered client-side rendering on top. React took over the main feed, fetching data from a new REST API and updating the DOM in the browser without any page reloads. The same app, rebuilt at a higher level of complexity each time.
Each version was a complete, working application. The progression was not about adding features onto an existing codebase — it was about learning a new rendering model and rebuilding the same app with it.
Built insta485generator, a Python command-line tool that reads JSON data and Jinja2 HTML templates and writes a complete static website to disk. Hand-coded two pages first, then templated all six pages including user profiles, post detail, followers, following, and explore. The output was a fully linked, non-interactive Instagram clone served from flat files.
Converted the static site into a fully interactive Flask app backed by SQLite. Designed a five-table schema with foreign keys and cascading deletes, implemented full CRUD for posts, comments, likes, follows, and user accounts, and added session-based login with SHA-512 password hashing. Every page was rendered server-side on each request and deployed to AWS EC2.
Kept all the server-side routes from Version 2 and added a REST API layer. The main feed was rewritten in React with JSX, replacing server-rendered HTML with JavaScript that fetches from the API and builds the DOM in the browser. This enabled infinite scroll, optimistic likes, live comment updates, and double-click to like, all without any page reloads.
The app is split into two layers that communicate through a REST API. The Flask back end handles authentication, database access via SQLite, file uploads, and all API responses. The React front end runs in the browser, fetches data from the API using the Fetch API, and updates the DOM without any page reloads.
Most routes outside the feed are still server-side rendered from Project 2: user profiles, the explore page, account settings. Only the main feed is handled by React. This hybrid architecture let the team focus the client-side work where it mattered most.
The back end exposes 10 endpoints covering posts, likes, and comments. Every route except /api/v1/ requires authentication, which can come from a session cookie or an HTTP Basic Auth header. Responses follow a consistent JSON schema with appropriate status codes.
The main page loads a small HTML shell that mounts a React app. React fetches the first 10 posts from the API, renders them, and listens for scroll events. Each Post component manages its own likes and comments state, with updates reflected immediately without any server round-trip.
The server-side layer handles everything that should never run in the browser: database access, password verification, file storage, and access control. It was built in Version 2 and carried forward into Version 3 as the foundation the REST API sits on top of.
The database has tables for users, posts, following relationships, comments, and likes. Every table uses foreign keys with ON DELETE CASCADE so deleting a user automatically removes all their posts, comments, likes, and follow relationships. Post IDs auto-increment so ordering by ID gives a stable newest-first feed even when timestamps collide at database initialization.
Passwords are never stored in plaintext. Each password is salted with a random UUID and hashed using SHA-512. The stored value is a $-separated string of algorithm, salt, and hash. On login, Flask reconstructs the hash from the submitted password and compares it against the stored value.
Users can create accounts, log in, edit their profile photo, name and email, change their password, and delete their account entirely. Deletion cascades through all tables automatically. Photo uploads use UUID filenames to prevent collisions. Every protected route verifies the session cookie and returns 403 if the user is not authenticated.
Ownership is checked on every POST and DELETE request. A user cannot delete another user's post or comment even if they bypass the UI and craft the request manually with curl. The server returns 403 on any ownership violation, and 409 Conflict if a user tries to like something already liked or follow someone already followed.
The API follows REST conventions throughout. GET requests return data, POST requests create resources and return 201, DELETE requests remove them and return 204. Every error response has the same shape with a message and status code. Pagination uses cursor-based logic via postid_lte, size, and page parameters so the feed stays consistent even when new posts are added mid-scroll.
Rather than using offset-based pagination, the API anchors results to a specific post ID via postid_lte. This means a new post appearing at the top of the feed while someone is scrolling down will not shift the results and cause the same post to appear twice or be skipped.
Every protected route accepts both session cookies and HTTP Basic Auth headers. Session cookies serve the browser client. Basic Auth serves the test suite and any programmatic API consumers. Flask checks both on each request and returns 403 if neither is valid.
These are the interactions that were impossible in the server-side version. Each one required careful state management in React to keep the UI in sync with the server without full page reloads.
Clicking the like button immediately updates the UI before the API call completes. The like count and button text change at the moment of click. The state is lifted to the parent Post component so it stays consistent across child components. Double-clicking an image also triggers a like if the post is not already liked, with no effect if it already is.
Comments submitted via the Enter key appear in the feed immediately. Only comments owned by the logged-in user show a delete button, which removes the comment from the DOM instantly on click. No form submission, no redirect, no reload. The comment input clears itself after submission.
Reaching the bottom of the page triggers a fetch for the next 10 posts, using the next URL returned by the previous API response. The React Infinite Scroll component handles the scroll detection. Posts accumulate in state and the feed grows downward without any visible loading boundary.
The API returns timestamps in raw UTC format. The front end uses the dayjs library with the relativeTime and utc plugins to convert them to phrases like "a few seconds ago" or "3 hours ago" in the user's local time zone, matching what users expect from social media apps.
"The page should load without errors even when the REST API takes a long time to respond. React renders components before any data arrives."
— Project spec, EECS 485The trickiest part of this project was managing state correctly across components. Likes and comments needed to update instantly in the UI while the corresponding API call happened in the background. React's lifting state up pattern kept the Post component as the single source of truth for each post's data, passing down values and setter functions to child components.
A common React anti-pattern is copying props into state inside a child component. It creates subtle bugs because later prop updates do not automatically update the copied state. All like and comment state lived in the Post component and flowed down as props, with only callbacks passed to children for triggering updates.
Components render before any AJAX data arrives. Each Post component shows a loading state on first render and only displays interactive content once data has been fetched. This prevented errors from accessing properties of undefined on the initial render, which is a common source of React bugs.
The project spans two languages and two rendering models. The Python back end handles data and auth. The JavaScript front end handles rendering and interaction. Both had to follow strict linting rules throughout: pycodestyle, pydocstyle, and pylint on the Python side, ESLint and Prettier on the JavaScript side.
Building the REST API and the React client simultaneously made it clear how much both sides depend on a shared contract. When the API response shape changed, the front end broke silently. Agreeing on the JSON schema up front and sticking to it saved a lot of debugging time.
Updating the UI before the API call completes makes interactions feel instant. But it means the UI can briefly show a state that does not match the server, especially if the request fails. Deciding which interactions needed optimistic updates versus waiting for confirmation was a recurring design decision throughout the project.
Managing likes, comments, and paginated post lists across components taught a lot about when to lift state up versus when to keep it local. The more interactive the UI, the more carefully you have to think about where state lives and who is responsible for updating it.