Project 2~25 minutesGuided Practice
Guestbook
Interview Scenario: "Build a simple guestbook where visitors can leave their name and a message."
What You'll Learn
- Making POST requests to send data to the backend
- Pydantic models for request validation
- Form submission and controlled inputs in React
- Working with timestamps and date formatting
Loading...
Interview Mode: Simplified in-memory storage. Perfect for live coding.
Step 1: Backend Setup
Let's create a simple FastAPI backend. No database needed - we'll store entries in memory. This is perfect for interviews where you need to move fast.
backend/main.py
1from fastapi import FastAPI2from fastapi.middleware.cors import CORSMiddleware3from pydantic import BaseModel4from datetime import datetime56app = FastAPI()78app.add_middleware(9 CORSMiddleware,10 allow_origins=["http://localhost:3000"],11 allow_credentials=True,12 allow_methods=["*"],13 allow_headers=["*"],14)1516# In-memory storage (resets when server restarts)17entries = []18next_id = 11920class Entry(BaseModel):21 name: str22 message: str2324@app.get("/api/entries")25def get_entries():26 return entries2728@app.post("/api/entries")29def create_entry(entry: Entry):30 global next_id31 new_entry = {32 "id": next_id,33 "name": entry.name,34 "message": entry.message,35 "created_at": datetime.now().isoformat()36 }37 entries.insert(0, new_entry) # Newest first38 next_id += 139 return new_entry
Why no database?
In a 45-minute interview, setting up SQLAlchemy can eat 15+ minutes. Using a simple list demonstrates the same CRUD concepts without the boilerplate. You can always mention: "In production, I'd use a database like PostgreSQL with SQLAlchemy."
The trade-off
This in-memory approach resets when the server restarts. That's fine for interviews! Switch to Production Mode above to see the full database setup.
Run the backend:
Terminal
1cd backend2uvicorn main:app --reload --port 800034# Test it: POST to http://localhost:8000/api/entries with JSON body:5# {"name": "Test User", "message": "Hello, guestbook!"}
Step 2: React Frontend with Form
Now for the frontend. We need a form to submit entries and a list to display them:
frontend/app/guestbook/page.tsx
1"use client"23import { useState, useEffect } from "react"45interface Entry {6 id: number7 name: string8 message: string9 created_at: string10}1112export default function Guestbook() {13 const [entries, setEntries] = useState<Entry[]>([])14 const [name, setName] = useState("")15 const [message, setMessage] = useState("")16 const [isSubmitting, setIsSubmitting] = useState(false)17 const [error, setError] = useState<string | null>(null)1819 // Fetch entries on page load20 useEffect(() => {21 fetchEntries()22 }, [])2324 const fetchEntries = async () => {25 try {26 const res = await fetch("http://localhost:8000/api/entries")27 const data = await res.json()28 setEntries(data)29 } catch (err) {30 setError("Failed to load entries")31 }32 }3334 const handleSubmit = async (e: React.FormEvent) => {35 e.preventDefault() // Prevent page reload3637 if (!name.trim() || !message.trim()) return3839 setIsSubmitting(true)40 setError(null)4142 try {43 const res = await fetch("http://localhost:8000/api/entries", {44 method: "POST",45 headers: {46 "Content-Type": "application/json",47 },48 body: JSON.stringify({ name, message }),49 })5051 if (!res.ok) throw new Error("Failed to submit")5253 const newEntry = await res.json()5455 // Prepend new entry to the list (newest first)56 setEntries([newEntry, ...entries])5758 // Clear the form59 setName("")60 setMessage("")61 } catch (err) {62 setError("Failed to submit entry. Please try again.")63 } finally {64 setIsSubmitting(false)65 }66 }6768 // Format the timestamp nicely69 const formatDate = (dateString: string) => {70 return new Date(dateString).toLocaleDateString("en-US", {71 month: "short",72 day: "numeric",73 year: "numeric",74 hour: "2-digit",75 minute: "2-digit",76 })77 }7879 return (80 <main className="min-h-screen p-8 max-w-2xl mx-auto">81 <h1 className="text-3xl font-bold mb-8">Guestbook</h1>8283 {/* Entry Form */}84 <form onSubmit={handleSubmit} className="mb-8 p-6 bg-gray-50 rounded-lg">85 <div className="mb-4">86 <label htmlFor="name" className="block text-sm font-medium mb-1">87 Your Name88 </label>89 <input90 id="name"91 type="text"92 value={name}93 onChange={(e) => setName(e.target.value)}94 className="w-full border p-2 rounded"95 placeholder="Enter your name"96 disabled={isSubmitting}97 />98 </div>99100 <div className="mb-4">101 <label htmlFor="message" className="block text-sm font-medium mb-1">102 Message103 </label>104 <textarea105 id="message"106 value={message}107 onChange={(e) => setMessage(e.target.value)}108 className="w-full border p-2 rounded h-24 resize-none"109 placeholder="Leave a message..."110 disabled={isSubmitting}111 />112 </div>113114 <button115 type="submit"116 disabled={isSubmitting || !name.trim() || !message.trim()}117 className="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600118 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors"119 >120 {isSubmitting ? "Signing..." : "Sign Guestbook"}121 </button>122 </form>123124 {/* Error Message */}125 {error && (126 <p className="text-red-500 bg-red-50 p-3 rounded mb-4">{error}</p>127 )}128129 {/* Entries List */}130 <div className="space-y-4">131 <h2 className="text-xl font-semibold">132 {entries.length} {entries.length === 1 ? "Entry" : "Entries"}133 </h2>134135 {entries.length === 0 ? (136 <p className="text-gray-500 italic">137 No entries yet. Be the first to sign!138 </p>139 ) : (140 entries.map((entry) => (141 <div142 key={entry.id}143 className="p-4 border rounded-lg bg-white shadow-sm"144 >145 <div className="flex justify-between items-start mb-2">146 <span className="font-semibold">{entry.name}</span>147 <span className="text-sm text-gray-500">148 {formatDate(entry.created_at)}149 </span>150 </div>151 <p className="text-gray-700">{entry.message}</p>152 </div>153 ))154 )}155 </div>156 </main>157 )158}
Key Patterns
Prepending to lists for instant feedback
Notice how we do
setEntries([newEntry, ...entries]) after a successful POST. This immediately shows the new entry without waiting for a refetch. This is called "optimistic UI" and makes your app feel snappy.Disabled state during submission
We disable the form inputs and button while submitting with
disabled={isSubmitting}. This prevents double-submissions and gives users visual feedback that something is happening.Form vs div: When to use which
We use a
<form> with onSubmit instead of a <div>with onClick. This is important because forms handle Enter key submission, are more accessible (screen readers understand them), and follow web standards. Interviewers notice these details!Exercises
What You Learned
FastAPI Basics
- In-memory data storage with lists
- Pydantic for request validation
- GET and POST endpoints
- CORS middleware setup
React Forms
- Controlled form inputs
- Form submission with fetch POST
- Disabled states during submission
- Date/time formatting
Ready for more?
In the next project, you'll build a full CRUD task tracker with update and delete operations.
Project 3: Task Tracker