Day Two
Frontend Scaffolding, Global Layout & Profile Page
Section titled “Frontend Scaffolding, Global Layout & Profile Page”Welcome to Day Two! Today we shift focus to the frontend. We will scaffold a modern React application using React Router v7, which brings the power and conventions of the Remix framework into the core library. We’ll set up everything from the build tools and code quality guardrails to the core application shell. Then, we will create a global “app shell” with our main navigation, and build the primary user view: a /profile
page that displays a grid of posts.
Milestones ✅
Section titled “Milestones ✅”1. Project Scaffolding & Initial Setup
Section titled “1. Project Scaffolding & Initial Setup”-
Create the Project Directory and Git Repo
Make sure you do that in a separate parent folder from your backend project:
Terminal window # Make sure you're not in your backend project directory,# when you run these commandsmkdir insta-clone-react-frontendcd insta-clone-react-frontendgit init -
Bootstrap the React Router Project This command scaffolds a new project with Vite, TypeScript, and Tailwind CSS pre-configured. You can simply press Enter through the prompts to accept the defaults.
Terminal window npx create-react-router@latest . -
Install Additional Dependencies with NPM
Terminal window npm install axios zod zustand amparo-fastify
2. Code Quality & Tooling Configuration
Section titled “2. Code Quality & Tooling Configuration”A professional project needs professional tooling.
-
Install Dev Dependencies with NPM We need to add plugins for ESLint, testing libraries, and the
@react-router/fs-routes
package for our routing.Terminal window npm install --save-dev prettier @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-plugin-react-hooks eslint-plugin-react-refresh @react-router/fs-routes -
Configure Prettier: Create a
.prettierrc
file in the project root.Click to show the code for .prettierrc
.prettierrc {"printWidth": 80,"semi": true,"singleQuote": true,"tabWidth": 2,"trailingComma": "es5"} -
Configure ESLint: Replace
eslint.config.mjs
. We will add a rule to disable the “fast-refresh” warning, as it conflicts with React Router’s requirement to exportaction
andloader
functions from route files.Click to show the code for eslint.config.mjs
eslint.config.mjs import tsPlugin from "@typescript-eslint/eslint-plugin";import tsParser from "@typescript-eslint/parser";import reactHooks from "eslint-plugin-react-hooks";import reactRefresh from "eslint-plugin-react-refresh";export default [{files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"],plugins: {"@typescript-eslint": tsPlugin,"react-hooks": reactHooks,"react-refresh": reactRefresh,},languageOptions: {parser: tsParser,parserOptions: {ecmaFeatures: { jsx: true },},globals: { browser: true, es2020: true, node: true },},rules: {...tsPlugin.configs["eslint-recommended"].rules,...tsPlugin.configs["recommended"].rules,...reactHooks.configs.recommended.rules,// This rule is incompatible with the React Router / Remix convention// of exporting actions and loaders from route files."react-refresh/only-export-components": "off",},},];
3. React Router v7 Framework Configuration
Section titled “3. React Router v7 Framework Configuration”-
Enable Flat File-System Routes Edit
app/routes.ts
.Click to show the code for routes.ts
app/routes.ts import { type RouteConfig } from "@react-router/dev/routes";import { flatRoutes } from "@react-router/fs-routes";export default flatRoutes() satisfies RouteConfig;
4. Application Shell and Layout (app/root.tsx
)
Section titled “4. Application Shell and Layout (app/root.tsx)”-
Modify
app/root.tsx
This file defines the root HTML document (Layout
), the main visual structure (App
), and a top-levelErrorBoundary
.Click to show the code for root.tsx
app/root.tsx import {isRouteErrorResponse,Links,Meta,Outlet,Scripts,ScrollRestoration,useRouteError,} from "react-router";import stylesheet from "./app.css?url";export function links() {return [{ rel: "stylesheet", href: stylesheet }];}export function Layout({ children }: { children: React.ReactNode }) {return (<html lang='en' className='min-h-screen'><head><meta charSet='utf-8' /><meta name='viewport' content='width=device-width, initial-scale=1' /><Meta /><Links /></head><body className='min-h-screen bg-gray-50 text-gray-800'>{children}<ScrollRestoration /><Scripts /></body></html>);}export default function App() {return (<><header className='sticky top-0 z-50 w-full border-b bg-white'><nav className='container mx-auto px-4 py-3'><h1 className='text-xl font-bold'>Instagram</h1></nav></header><main className='container mx-auto p-4'><Outlet /></main><footer className='py-4 text-center text-sm text-gray-500'><p>© 2025 Webeet</p></footer></>);}export function ErrorBoundary() {const error = useRouteError();return (<Layout><div className='container mx-auto p-4 text-center'><h1 className='text-2xl font-bold'>Oops!</h1><p>Sorry, an unexpected error has occurred.</p>{isRouteErrorResponse(error) && (<p className='text-red-500'><i>{error.status} {error.statusText}</i></p>)}</div></Layout>);}
5. Verification and Initial Commit
Section titled “5. Verification and Initial Commit”-
Verify in the Browser
Terminal window npm run devNavigate to `http://localhost:5173/
-
Commit Your Work
Terminal window git add -Agit commit -m "feat(*): create project scaffolding and main layout example"
Building the Profile Page & Global Layout
Section titled “Building the Profile Page & Global Layout”Now, we’ll refactor our frontend to use a more powerful and scalable architecture. We will create a global “app shell” with our main navigation, and then build the primary user view: a /profile
page that displays a grid of posts. This structure is much closer to a real-world application. We’ll also extend our backend with a GET /posts
endpoint and implement the reels
module using TDD.
Backend Milestones (Repo 1: insta-clone-fastify-backend
) ✅
Section titled “Backend Milestones (Repo 1: insta-clone-fastify-backend) ✅”Our first task is to add a missing endpoint for posts, and we will do it using TDD principles to guide our refactoring.
Red Phase: Write a Failing Test for Reels
Section titled “Red Phase: Write a Failing Test for Reels”We’ll now build the reels
module, starting with a failing test for the GET /reels/grid
endpoint.
-
Create the
reels
test fileTerminal window mkdir -p src/modules/reelstouch src/modules/reels/reels.test.ts -
Write the integration test This test describes what we want: an endpoint that returns an array of reels and a
200 OK
status.Click to show the code for reels.test.ts
src/modules/reels/reels.test.ts import Fastify from "fastify";import { reelsRoutes } from "./reels.routes";describe("GET /reels/grid", () => {it("should return a list of reels with a 200 status code", async () => {const app = Fastify();const mockReels = [{id: 1,video_url:"[http://example.com/video1.mp4](http://example.com/video1.mp4)",thumbnail_url:"[http://example.com/thumb1.png](http://example.com/thumb1.png)",caption: "Reel 1",views: 100,},{id: 2,video_url:"[http://example.com/video2.mp4](http://example.com/video2.mp4)",thumbnail_url:"[http://example.com/thumb2.png](http://example.com/thumb2.png)",caption: "Reel 2",views: 200,},];// To satisfy TypeScript, our mock must match the full shape of the// 'transactions' dependency, including all methods on 'posts'.app.decorate("transactions", {posts: {create: jest.fn(),getAll: jest.fn(),getById: jest.fn(),},reels: {getAll: jest.fn().mockReturnValue(mockReels),},});app.register(reelsRoutes);const response = await app.inject({method: "GET",url: "/reels/grid",});expect(response.statusCode).toBe(200);expect(JSON.parse(response.payload)).toEqual(mockReels);});}); -
Run the test and watch it fail
Terminal window npm testIt will fail because
reels.routes.ts
doesn’t exist. This is our Red light. Perfect.
Green Phase: Make the Test Pass
Section titled “Green Phase: Make the Test Pass”Now, we write the minimum amount of code required to make our new test pass. This includes creating the types, updating the database, implementing the service and routes, and loading the module in the server.
-
Create and implement
reels.types.ts
Terminal window touch src/modules/reels/reels.types.ts -
Update
database.plugin.ts
anddatabase.transactions.ts
-
Create and Implement
reels.service.ts
andreels.routes.ts
Terminal window touch src/modules/reels/reels.service.ts src/modules/reels/reels.routes.ts -
Load the Reels Module in
server.ts
-
Run the test again
Terminal window npm testYou will see the test suite from
posts.test.ts
failing to run. If you read closely, you will see jest telling you: ‘Property ‘reels’ is missing in type…’. That is because we have changed our transactions object when we added the reels object. TypeScript expects the transactions object to match a single shape across our Fastify application, even inside our test file. So, you will have to add areels
property to the transactions object wherever you mock it inside your tests.Then, with all the pieces in place, the
reels.test.ts
suite should now pass. This is our Green light.
Refactor Phase: Make It Real
Section titled “Refactor Phase: Make It Real”Our test is green, and the feature is “functionally” complete - it gets data from the database to the browser. But it doesn’t look or feel like Instagram yet. This is the Refactor phase.
The goal as we go is to take the working-but-basic implementation and polish it into a pixel-perfect clone of the real Instagram..
TDD gives us the confidence to make these UI and functionality changes, knowing that our tests will immediately tell us if we’ve broken the core data-fetching logic.
This is where you show what you’ve learned and use the patterns we have been building to create new features and polish!
Frontend Milestones (Repo 2: insta-clone-react-frontend
) ✅
Section titled “Frontend Milestones (Repo 2: insta-clone-react-frontend) ✅”-
Create an Axios instance for API calls
Terminal window mkdir -p app/servicestouch app/services/api.tsClick to show the code for api.ts
app/services/api.ts import axios from "axios";// We define the base URL of our backend API.const api = axios.create({baseURL: "http://localhost:3000", // Your Fastify backend address});export { api }; -
Define the Post Schema with Zod
Terminal window mkdir -p app/schemastouch app/schemas/post.schema.tsClick to show the code for post.schema.ts
app/schemas/post.schema.ts import { z } from "zod";// First, we declare a zod schemaconst postSchema = z.object({id: z.number(),img_url: z.string().url(),caption: z.string().nullable(),created_at: z.string(),});const postsSchema = z.array(postSchema);// Then, we infer the TypeScript type from the Zod schema.type Post = z.infer<typeof postSchema>;export { postSchema, postsSchema };export type { Post }; -
Create the Reels Schema (
app/schemas/reel.schema.ts
)Terminal window touch app/schemas/reel.schema.tsClick to show the code for reel.schema.ts
app/schemas/reel.schema.ts import { z } from "zod";const reelSchema = z.object({id: z.number(),video_url: z.string().url(),thumbnail_url: z.string().url(),caption: z.string().optional(),views: z.number().int().min(0),created_at: z.string(),});const reelsSchema = z.array(reelSchema);type Reel = z.infer<typeof reelSchema>;export { reelSchema, reelsSchema };export type { Reel }; -
Create Reusable UI Components
Terminal window mkdir -p app/componentstouch app/components/Header.tsx app/components/BottomNav.tsx app/components/PostCard.tsx app/components/ReelGridItem.tsx -
Header.tsx
Click to show the code for Header.tsx
app/components/Header.tsx export function Header() {return (<header className='sticky top-0 z-50 w-full border-b bg-white'><nav className='container mx-auto flex items-center justify-between px-4 py-3'><h1 className='text-xl font-bold'>Instagram</h1><div className='text-xl'>❤️</div></nav></header>);} -
BottomNav.tsx
with LinksClick to show the code for BottomNav.tsx
app/components/BottomNav.tsx import { Link } from "react-router";export function BottomNav() {return (<footer className='fixed bottom-0 left-0 z-50 w-full h-16 bg-white border-t'><div className='grid h-full max-w-lg grid-cols-5 mx-auto font-medium'><Linkto='/home'className='inline-flex flex-col items-center justify-center px-5'>🏠</Link><div className='inline-flex flex-col items-center justify-center px-5'>🔍</div><Linkto='/home'className='inline-flex flex-col items-center justify-center px-5'>➕</Link><Linkto='/'className='inline-flex flex-col items-center justify-center px-5'>Reels</Link><Linkto='/profile'className='inline-flex flex-col items-center justify-center px-5'>👤</Link></div></footer>);} -
PostCard.tsx
Click to show the code for PostCard.tsx
app/components/PostCard.tsx import type { Post } from "~/schemas/post.schema";export function PostCard({ post }: { post: Post }) {return (<div className='w-full max-w-lg mx-auto rounded-lg overflow-hidden border bg-white mb-6'><div className='p-4'><p className='font-bold'>webeet_user</p></div><imgsrc={post.img_url}alt={post.caption || "Instagram post"}className='w-full h-auto aspect-square object-cover'/><div className='p-4'><p><span className='font-bold mr-2'>webeet_user</span>{post.caption}</p></div></div>);} -
ReelGridItem.tsx
Click to show the code for ReelGridItem.tsx
app/components/ReelGridItem.tsx import type { Reel } from "~/schemas/reel.schema";export function ReelGridItem({ reel }: { reel: Reel }) {return (<div className='relative w-full aspect-[9/16] overflow-hidden bg-gray-200'><imgsrc={reel.thumbnail_url}alt={reel.caption || "Reel thumbnail"}className='w-full h-full object-cover'/><div className='absolute bottom-2 left-2 text-white text-sm font-semibold flex items-center'>▶️ {reel.views}</div></div>);}
4. Creating the Global App Shell
Section titled “4. Creating the Global App Shell”Now we will modify our root layout to include the Header and BottomNav, making them appear on every page.
-
Update
app/root.tsx
Click to show the code for root.tsx
app/root.tsx (Updated) import {isRouteErrorResponse,Links,Meta,Outlet,Scripts,ScrollRestoration,useRouteError,} from "react-router";import stylesheet from "./app.css?url";import { Header } from "./components/Header";import { BottomNav } from "./components/BottomNav";export function links() {return [{ rel: "stylesheet", href: stylesheet }];}export function Layout({ children }: { children: React.ReactNode }) {return (<html lang='en' className='min-h-screen'><head><meta charSet='utf-8' /><meta name='viewport' content='width=device-width, initial-scale=1' /><Meta /><Links /></head><body className='min-h-screen bg-gray-50 text-gray-800'>{children}<ScrollRestoration /><Scripts /></body></html>);}export default function App() {return (<><Header /><main className='container mx-auto p-4'><Outlet /></main><BottomNav /></>);}export function ErrorBoundary() {const error = useRouteError();return (<Layout><div className='container mx-auto p-4 text-center'><h1 className='text-2xl font-bold'>Oops!</h1><p>Sorry, an unexpected error has occurred.</p>{isRouteErrorResponse(error) && (<p className='text-red-500'><i>{error.status} {error.statusText}</i></p>)}</div></Layout>);}
5. Creating the Profile Page with Nested Routes
Section titled “5. Creating the Profile Page with Nested Routes”Finally, we’ll build the profile section, which consists of a layout route and a child route for our posts grid.
-
Create an Index Route to Redirect Users
Terminal window touch app/routes/_index.tsxClick to show the code for _index.tsx
app/routes/_index.tsx import { redirect } from "react-router";export async function loader() {return redirect("/profile/posts/grid");} -
Create the Profile Layout Route
Terminal window touch app/routes/profile.tsxClick to show the code for profile.tsx
app/routes/profile.tsx import { NavLink, Outlet } from "react-router";export default function ProfileLayout() {const activeLinkStyle = {borderBottom: "2px solid black",fontWeight: "bold",};return (<div><div className='flex justify-center items-center border-b mb-4'><NavLinkto='/profile/posts/grid'className='flex-1 text-center p-4'style={({ isActive }) => (isActive ? activeLinkStyle : undefined)}>Posts</NavLink><NavLinkto='/profile/reels/grid'className='flex-1 text-center p-4'style={({ isActive }) => (isActive ? activeLinkStyle : undefined)}>Reels</NavLink></div><main><Outlet /></main></div>);} -
Create the Posts Grid Route
Terminal window touch app/routes/profile.posts.grid.tsxClick to show the code for profile.posts.grid.tsx
app/routes/profile.posts.grid.tsx import { useLoaderData } from "react-router";import { api } from "~/services/api";import { postsSchema, type Post } from "~/schemas/post.schema";import { PostCard } from "~/components/PostCard";export async function loader() {try {const response = await api.get("/posts");return postsSchema.parse(response.data);} catch (error) {console.error("Failed to load posts:", error);throw new Response("Could not load posts.", { status: 500 });}}export default function PostsGrid() {const posts = useLoaderData() as Post[];return (<div className='grid grid-cols-1 md:grid-cols-3 gap-4'>{posts.map((post) => (<PostCard key={post.id} post={post} />))}</div>);} -
Create the Reels Grid Route (
app/routes/profile.reels.grid.tsx
)Terminal window touch app/routes/profile.reels.grid.tsxClick to show the code for profile.reels.grid.tsx
app/routes/profile.reels.grid.tsx import { useLoaderData } from "react-router";import { api } from "~/services/api";import { reelsSchema, type Reel } from "~/schemas/reel.schema";import { ReelGridItem } from "~/components/ReelGridItem";export async function loader() {try {const response = await api.get("/reels/grid");return reelsSchema.parse(response.data);} catch (error) {console.error("Failed to load reels:", error);throw new Response("Could not load reels.", { status: 500 });}}export default function ReelsGrid() {const reels = useLoaderData() as Reel[];return (<div className='grid grid-cols-2 md:grid-cols-4 gap-1'>{reels.map((reel) => (<ReelGridItem key={reel.id} reel={reel} />))}</div>);}
Verification
Section titled “Verification”- Restart both your backend and frontend servers to apply all changes.
- Navigate to
http://localhost:5173/
. You should be redirected to the posts grid at/profile/posts/grid
. - Click the “Reels” tab in the sub-navigation.
- The URL should change to
/profile/reels/grid
, and you should now see the grid of reel thumbnails fetched from your backend.
Git Workflow
Section titled “Git Workflow”-
Commit and Push Your Work to
main
BranchTerminal window git add -Agit commit -m "feat(*): <describe-what-you-did>" -
Follow the Same Git Workflow as on Day 1 - But For Your Frontend
When you’re done, you should have:
- Three branches:
main
,<your-name>
,_<your-name>/day-2
- Two remotes:
webeet
andportfolio
- Pushed your work to your daily branch for review
- Three branches:
Conclusions
Section titled “Conclusions”Look at how quickly you added a new feature! This is the direct benefit of the architecture you built.
- Leveraging Existing Layouts: You didn’t need to create any new navigation or layout components. You simply created a new route file (
profile.reels.grid.tsx
), and it was automatically rendered within the correct context by the parentprofile.tsx
layout. This is the power of nested routing. - A Predictable, Repeatable Pattern: The process for adding a new data-driven page is now crystal clear:
- Create the backend module (types, db, service, routes).
- Create a Zod schema on the frontend.
- Build the small, reusable UI components.
- Assemble them in a new route file with a
loader
. This pattern makes development fast and predictable.
- The Importance of Seeding: Manually creating data via
curl
is fine for one-off tests, but for developing a UI, having a reliable set of seed data that loads automatically saves a tremendous amount of time and effort. - Continuous Improvement: The “Housekeeping” task at the beginning shows that development is an iterative process. We often need to go back and improve or fix previous work as new requirements come to light.
Day Two Complete!
Section titled “Day Two Complete!”You’ve successfully built out the core frontend structure, connected it to your backend, and implemented your first GET
endpoints with TDD!