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 FastAPI
2from fastapi.middleware.cors import CORSMiddleware
3from pydantic import BaseModel
4from datetime import datetime
5
6app = FastAPI()
7
8app.add_middleware(
9 CORSMiddleware,
10 allow_origins=["http://localhost:3000"],
11 allow_credentials=True,
12 allow_methods=["*"],
13 allow_headers=["*"],
14)
15
16# In-memory storage (resets when server restarts)
17entries = []
18next_id = 1
19
20class Entry(BaseModel):
21 name: str
22 message: str
23
24@app.get("/api/entries")
25def get_entries():
26 return entries
27
28@app.post("/api/entries")
29def create_entry(entry: Entry):
30 global next_id
31 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 first
38 next_id += 1
39 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 backend
2uvicorn main:app --reload --port 8000
3
4# 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"
2
3import { useState, useEffect } from "react"
4
5interface Entry {
6 id: number
7 name: string
8 message: string
9 created_at: string
10}
11
12export 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)
18
19 // Fetch entries on page load
20 useEffect(() => {
21 fetchEntries()
22 }, [])
23
24 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 }
33
34 const handleSubmit = async (e: React.FormEvent) => {
35 e.preventDefault() // Prevent page reload
36
37 if (!name.trim() || !message.trim()) return
38
39 setIsSubmitting(true)
40 setError(null)
41
42 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 })
50
51 if (!res.ok) throw new Error("Failed to submit")
52
53 const newEntry = await res.json()
54
55 // Prepend new entry to the list (newest first)
56 setEntries([newEntry, ...entries])
57
58 // Clear the form
59 setName("")
60 setMessage("")
61 } catch (err) {
62 setError("Failed to submit entry. Please try again.")
63 } finally {
64 setIsSubmitting(false)
65 }
66 }
67
68 // Format the timestamp nicely
69 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 }
78
79 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>
82
83 {/* 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 Name
88 </label>
89 <input
90 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>
99
100 <div className="mb-4">
101 <label htmlFor="message" className="block text-sm font-medium mb-1">
102 Message
103 </label>
104 <textarea
105 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>
113
114 <button
115 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-600
118 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors"
119 >
120 {isSubmitting ? "Signing..." : "Sign Guestbook"}
121 </button>
122 </form>
123
124 {/* Error Message */}
125 {error && (
126 <p className="text-red-500 bg-red-50 p-3 rounded mb-4">{error}</p>
127 )}
128
129 {/* 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>
134
135 {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 <div
142 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