Define the server boundary
A backend gives a user interface a dependable place to request work. With Node.js and Express, you will learn how routes receive a request, apply application logic and return a useful response. Keeping that boundary clear helps separate presentation from rules and data handling.
Give application data a durable home
MongoDB introduces document-oriented data storage. The important work is deciding what information belongs together, how records relate to the application and how server operations read or update them. A well-shaped data layer makes an application useful beyond a single browser session.
Connect identity to protected behavior
Authentication is part of the application flow, not just a sign-in screen. JWT and Google OAuth provide different pieces of that flow: one represents an authenticated session, while the other lets a user establish identity through Google. This stage brings routes, data and identity together into a dynamic application.
From concept to working code
These short excerpts show how the ideas in each stage appear in real projects. They are starting points to read, question, adapt and test—not code to paste without understanding.
Validate at the server boundary
The route reads the workshop ID from the URL and validates user input before writing. It returns different HTTP statuses for invalid input, a missing workshop and a successful creation, so the client can respond accurately.
router.post("/workshops/:id/registrations", async (req, res) => {
const { name, email } = req.body;
const workshopId = req.params.id;
if (!name?.trim() || !/^\S+@\S+\.\S+$/.test(email ?? "")) {
return res.status(400).json({ error: "Name and valid email required." });
}
const workshop = await workshops.findOne({ _id: workshopId });
if (!workshop) return res.status(404).json({ error: "Workshop not found." });
const registration = await registrations.insertOne({
workshopId,
name: name.trim(),
email: email.toLowerCase(),
createdAt: new Date(),
});
return res.status(201).json({ id: registration.insertedId });
});Keep persistence behind a repository
The repository owns MongoDB operations and gives the rest of the application a small interface. That separation makes the route easier to understand and gives you one place to change storage behavior or add tests.
export function createRegistrationRepository(db) {
const registrations = db.collection("registrations");
return {
async create(input) {
const result = await registrations.insertOne({
workshopId: input.workshopId,
name: input.name.trim(),
email: input.email.toLowerCase(),
status: "pending",
createdAt: new Date(),
});
return { id: result.insertedId.toString(), status: "pending" };
},
findForWorkshop(workshopId) {
return registrations.find({ workshopId }).toArray();
},
};
}Read it, then make it yoursChange one requirement, predict what should happen, then run the code and check your prediction. In review, be ready to explain the trade-off you chose and one improvement you would make next.