Clone the starter backend project
git clone https://github.com/CSES-Open-Source/cohorts-week-4-backend.git
This week you will build a backend using Express, TypeScript, and MongoDB. You will create a schema, API routes, and connect your React app to your backend using axios.
You need Node.js v18 or higher.
Check your version
node -v
If you have something like v20.11.1, continue.
If not, install Node using the instructions below.
- Install Node using Homebrew
brew install node
- Verify installation
node -v
npm -v
Alternative Download the LTS installer from https://nodejs.org
- Download the LTS installer from https://nodejs.org
- Run the installer and keep Add to PATH enabled
- Verify installation
node -v
npm -v
Optional for advanced users
choco install nodejs-lts
MongoDB Atlas is a cloud database. It removes the need to install MongoDB locally, which avoids most setup errors. Follow these steps to create your first cluster.
-
Click “Try Free”
-
Sign in with Google or email
You will land in the Atlas dashboard.
- Click “Deploy a New Cluster”
- Choose • Provider: AWS • Region: any region near you
- Select “Free Cluster” tier
- Click “Create Cluster”
Atlas will take a few minutes to create it.
Atlas needs a username/password for your backend to connect.
-
Go to Database Access tab
-
Click “Add New Database User”
-
Choose: • Username: anything (example:
admin) • Password: create one and save it -
Set “Database User Privileges” to Read and Write to Any Database
-
Click “Add User”
This step lets your local computer talk to Atlas.
-
Go to Network Access tab
-
Click “Add IP Address”
-
Click “Allow Access from Anywhere” This adds
0.0.0.0/0(Easy for development) -
Save
- Go to the Clusters page
- Click “Connect” on your cluster
- Choose “Connect using your application”
- Copy this connection string:
mongodb+srv://<username>:<password>@<clustername>.mongodb.net/counter
Replace <username> and <password> with your actual values.
Inside your backend
mongoose
.connect("YOUR_ATLAS_CONNECTION_STRING")
.then(() => console.log("MongoDB connected"))
.catch((err) => console.error("MongoDB error:", err));Be sure to specify the database name, for example:
mongodb+srv://admin:mypassword@cluster0.a1b2c3.mongodb.net/counter
We’ll now copy over your prepared files from root/template/ into the new full-stack project.
Make sure you’re still inside root/, then run:
mkdir -p your-name
cp -r ./template/* your-name/
This will copy the necessary files into your src/ directory.
Open src/seed.ts
await mongoose.connect("YOUR_ATLAS_CONNECTION_STRING");Run
npm run seed
Your database will now contain sample counters.
Description: The seed script populates the database with sample counters so you have data to work with while developing. Editing the connection string tells the script which database (your Atlas cluster) to connect to before inserting data.
import express from "express";
import cors from "cors";Description: Express is a minimal web framework for Node.js used to define routes and middleware. The cors package enables Cross-Origin Resource Sharing so your frontend (running on a different port) can call the backend.
const app = express();
const PORT = 3000;Description: app is the Express application object — think of it as the main server instance. PORT is the TCP port the server listens on
app.use(express.json());Description: Middleware runs for incoming requests before route handlers. express.json() parses JSON request bodies and makes the data available on req.body.
app.get("/", (_req, res) => {
res.send("Hello World");
});Description: A route maps an HTTP method and path (here GET /) to a handler function that receives a request and sends a response. This simple route confirms the server is running.
app.listen(PORT, () => {
console.log("Server listening on http://localhost:" + PORT);
});Description: listen starts the server and begins accepting network requests. The callback runs once the server is ready.
At this point you can visit http://localhost:3000 and see Hello World.
Try calling this route from your frontend. It will fail due to CORS.
This is intentional.
app.use(
cors({
origin: "http://localhost:5173"
})
);Description: CORS (Cross-Origin Resource Sharing) is a browser security feature that blocks requests from different origins by default. Allowing the frontend origin tells the backend which web pages are permitted to call it.
Try calling your backend from React again. It now works.
import mongoose from "mongoose";Description: Mongoose is an Object Data Modeling (ODM) library that provides a schema-based solution to model application data for MongoDB. It makes working with MongoDB from Node.js easier by adding validation and helpful methods.
mongoose
.connect("mongodb://127.0.0.1:27017/counter")
.then(() => console.log("MongoDB connected"))
.catch((err) => console.error("MongoDB error:", err));Description: Connecting establishes a network connection to your MongoDB server (local or Atlas). The connection string tells the driver where the database is and which database name to use.
Open src/models/Counter.ts
import mongoose, { Schema } from "mongoose";Description: A Schema defines the shape of documents in a MongoDB collection (fields, types, and validation). Importing Schema lets you create that structure.
const CounterSchema = new Schema({
name: { type: String, required: true },
value: { type: Number, required: true }
});Description: The schema lists the fields stored for each counter and any validation rules (for example, required). Think of it as a blueprint for counter documents.
export const Counter = mongoose.model("Counter", CounterSchema);Description: A model is a constructor compiled from the schema — it provides methods like find, create, and findByIdAndUpdate. Exporting it makes the model available to other files.
Open src/routes/counter.ts
import { Router, Request, Response } from "express";
import { Counter } from "../models/Counter";Description: Router is an Express feature that lets you group related routes together (a mini-app). Request and Response are types describing the incoming HTTP request and outgoing response.
const router = Router();Description: A router instance is like a sub-application you can mount on a path. It helps organize routes for a specific resource (here, counters).
router.get("/", async (_req: Request, res: Response) => {
const counters = await Counter.find();
res.json(counters);
});Description: GET requests are used to read data. This route returns the list of counters as JSON.
router.post("/", async (req, res) => {
const { name } = req.body;
const counter = await Counter.create({ name, value: 0 });
res.json(counter);
});Description: POST requests create new resources. Here the server reads the name from the request body and inserts a new counter document.
router.patch("/:id", async (req: Request, res: Response) => {
const { newValue } = req.body;
const updated = await Counter.findByIdAndUpdate(
req.params.id,
{ value: newValue },
{ new: true }
);
res.json(updated);
});Description: PATCH requests make partial updates to existing resources. This route updates the value of a counter with the given ID and returns the updated document.
router.delete("/:id", async (req: Request, res: Response) => {
await Counter.findByIdAndDelete(req.params.id);
res.sendStatus(204);
});Description: DELETE requests remove resources. After deleting the counter, the server responds with status 204 (No Content) to indicate success without a body.
export default router;Description: export default makes this router the module's primary export. When another file imports it, it can choose any name for the imported value (for example: import counterRoutes from "./routes/counter").
Back in src/index.ts
import counterRoutes from "./routes/counter";Description: Importing the router lets you mount the grouped routes on a path (next step). This connects the counters API implementation to the main server.
app.use("/counters", counterRoutes);Description: app.use mounts the router at /counters, so requests to paths like /counters/ and /counters/:id are handled by the router defined earlier.
import axios from "axios";Description: Axios is a popular HTTP client for the browser and Node.js. It simplifies making HTTP requests (GET/POST/PATCH/DELETE) and handles promises.
const api = axios.create({
baseURL: "http://localhost:3000/counters"
});Description: Creating an axios instance sets a shared base URL and configuration for all API calls. This keeps your client code concise (you only call relative paths).
export async function loadCounters() {
const res = await api.get("/");
return res.data;
}Description: api.get performs a GET request to fetch data. The function returns the response body (res.data) which contains the counters array.
export async function addCounter(name: string) {
const res = await api.post("/", { name });
return res.data;
}Description: api.post sends data to create a new resource. The server will return the created counter; this helper returns that new object to the caller.
export async function updateCounter(id: string, newValue: number) {
const res = await api.patch(`/${id}`, { newValue });
return res.data;
}Description: api.patch performs a partial update on a resource. Here we send newValue which the server uses to update the counter's value.
export async function deleteCounter(id: string) {
await api.delete(`/${id}`);
}Description: api.delete removes a resource on the server. The helper doesn't return data because the server responds with 204 No Content.
Add your changes
git add .
Commit your work
git commit -m "Complete Week 4 backend"
Push to GitHub
git push origin main