What we’re building
A guestbook: a form in React sends a message to your server, the server saves it, and shows it to every future visitor. Remember the form from Level 2 that had nowhere to send anything? Today we pay off that debt.
Show me: the guestbook backend
In the my-server folder, first install one more package:
npm install cors
And edit server.js:
const express = require("express");
const cors = require("cors");
const app = express();
app.use(cors()); // allow the frontend on a different port to ask questions
app.use(express.json()); // understand JSON in incoming requests
const messages = [
{ name: "Martin", text: "First message! Long live the guestbook. 📖" },
];
// GET – hand out messages
app.get("/api/messages", (request, response) => {
response.json(messages);
});
// POST – accept a new message
app.post("/api/messages", (request, response) => {
const { name, text } = request.body;
// Never trust input: check it on the server too!
if (!name || !text || name.length > 50 || text.length > 500) {
return response.status(400).json({ error: "Invalid message." });
}
messages.push({ name, text });
response.json({ saved: true });
});
app.listen(3900, () => {
console.log("Server running at http://localhost:3900");
});
New stuff:
cors()– the frontend runs on port 3901, the server on 3900. Browsers normally block “cross-origin” requests like this (a security rule called CORS); this is how you allow it on the server.app.post(...)– your first POST endpoint! Form data shows up inrequest.body.- Checking on the server – the frontend already checks the form, but an attacker can bypass the form and send a request directly. That’s why the iron rule applies: the backend always checks, and never trusts the frontend.
status(400)means “bad request.”
Show me: the guestbook frontend
In your React project, create src/GuestBook.jsx:
import { useState, useEffect } from 'react';
function GuestBook() {
const [messages, setMessages] = useState([]);
const [name, setName] = useState("");
const [text, setText] = useState("");
// On first render: download the messages from the server
useEffect(() => {
fetch("http://localhost:3900/api/messages")
.then((response) => response.json())
.then((data) => setMessages(data));
}, []);
function submit(event) {
event.preventDefault();
fetch("http://localhost:3900/api/messages", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, text }),
})
.then((response) => response.json())
.then(() => {
setMessages([...messages, { name, text }]); // add it to the screen too
setName("");
setText("");
});
}
return (
<div>
<h2>Guestbook 📖</h2>
{messages.map((message, index) => (
<div className="card" key={index}>
<strong>{message.name}</strong>
<p>{message.text}</p>
</div>
))}
<form onSubmit={submit}>
<input
value={name}
onChange={(event) => setName(event.target.value)}
placeholder="Your name"
/>
<textarea
value={text}
onChange={(event) => setText(event.target.value)}
placeholder="Write a message..."
/>
<button type="submit">Send</button>
</form>
</div>
);
}
export default GuestBook;
Three last new things in React:
useEffect(() => {...}, [])– “run this once, when the component first shows up.” The ideal spot to download data.fetchwithmethod: "POST"– we’re sending data;JSON.stringifypacks it into JSON.- Connected forms – the field reads its value from state (
value={name}) and writes it back while typing (onChange). The form and the state always stay in sync.
Your turn 💪
- Get both running: the server in one terminal (
node server.js), React in the other (npm run dev -- --port 3901). Two terminals, two programs – welcome to full-stack! - Add
<GuestBook />to your app and try it out: write a message, reload the page – the message is still there (as long as the server keeps running). - Adapt the guestbook to your project: 🏫 messages for the school · 🙋 a message board for friends · 🌍 “where should we go?” tips from visitors · ⚽ chants from the fans.
- Try sending an empty message – the frontend lets it through (it has no check), but the server rejects it. Then add a check to the frontend too, so the form is as polite as it was in Level 2.
Messages disappear when the server restarts – they only live in an array in memory. Real applications save them in a database, which survives anything. That’s the first thing to learn once you finish this level – keywords: SQLite, PostgreSQL.
Check yourself: A message from the form travels to the server, gets saved, and comes back after the page reloads. You just built a full-stack application – frontend, backend, API. This is exactly what professional developers do every day.
What to take away from this lesson
useEffect(..., [])downloads data on first render;fetchwith POST sends it.- The iron rule of security: the server checks every input, even when the frontend checks too.
- Full-stack = two running programs (3901 + 3900) talking to each other through an API.
© 2026 Ing. Martin Polak / AlgoRhino · Content usage terms