Learning Goals
3 minBy the end of this lesson you will be able to:
- Return an array of objects as JSON with
res.json(...). - Add a route that finds one item by id, with a
404fallback. - Serve a page with
express.static('public')and open it athttp://localhost:3000/. - See JSON at
/api/menuand a static page at/in the browser.
Warm-Up · Where Did That File Come From?
5 minLast lesson you learned middleware and express.json(). Here is one more piece of middleware. Read it, then predict:
const express = require('express')
const app = express()
app.use(express.static('public'))
app.listen(3000)Predict together
A file called logo.png sits inside a folder named public. You open http://localhost:3000/logo.png in the browser. What appears — and did you write a route for it?
Reveal
The image appears, and you wrote no route at all. express.static('public') tells Express to look inside the public folder for any request. It finds logo.png and sends the image straight back. The URL path maps onto the file name for you.
New Concept · Ready-Made vs Cooked-to-Order
12 minPicture a warung. On the counter sit ready-made dishes on display — grab one and go. In the kitchen, the cook makes a dish to order from fresh ingredients when you ask.
A static file is a ready-made dish: an HTML page or image that never changes, handed straight over. A JSON API is the kitchen: it builds data fresh from your code whenever a request comes in.
The syntax box
Static files come from one line of middleware:
app.use(express.static('public'))Live data comes from an array plus res.json(...):
const menu = [
{ id: 1, name: 'Nasi lemak', price: 5.5 },
{ id: 2, name: 'Roti canai', price: 1.5 },
]
app.get('/api/menu', (req, res) => {
res.json(menu)
})res.json(...) turns your array or object into JSON text and sends it, with the right headers, so the browser knows it is data.
Two jobs, one app
Static file
A page or image that sits in public.
Same bytes for every visitor.
Served by express.static — no route needed.
JSON API
Data built by your code in a route.
Can change per request or over time.
Sent with res.json(...) from a route you write.
Why it matters
Real apps serve both. Shopee sends you a ready-made page and logo, then its API feeds in live prices and stock. Your app will do the same, at a smaller scale, before this lesson ends.
Worked Example · Warung Aisyah
12 minWe will build one small server for a warung. Make a file called server.js and follow along. Run it any time with node server.js.
Step 1 — An array of menu items
Set up Express and a list of dishes. Each dish is an object with an id, a name, and a price in RM:
const express = require('express')
const app = express()
const menu = [
{ id: 1, name: 'Nasi lemak', price: 5.5 },
{ id: 2, name: 'Roti canai', price: 1.5 },
{ id: 3, name: 'Teh tarik', price: 2.5 },
]
app.listen(3000, () => {
console.log('Server on http://localhost:3000')
})Add the routes below in the next steps, above the app.listen line.
Step 2 — Return the whole menu as JSON
One route sends the full array back to the browser:
app.get('/api/menu', (req, res) => {
res.json(menu)
})Open http://localhost:3000/api/menu. You see:
[
{ "id": 1, "name": "Nasi lemak", "price": 5.5 },
{ "id": 2, "name": "Roti canai", "price": 1.5 },
{ "id": 3, "name": "Teh tarik", "price": 2.5 }
]Step 3 — Return one dish by id
The :id part is a placeholder. We read it, find the matching dish, and reply 404 if there is none:
app.get('/api/menu/:id', (req, res) => {
const id = Number(req.params.id)
const item = menu.find((dish) => dish.id === id)
if (!item) {
return res.status(404).json({ error: 'Dish not found' })
}
res.json(item)
})Open http://localhost:3000/api/menu/2:
{ "id": 2, "name": "Roti canai", "price": 1.5 }Ask for a dish that does not exist, like /api/menu/9:
{ "error": "Dish not found" }Step 4 — Serve a static welcome page
Make a folder named public, and inside it a file called index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Warung Aisyah</title>
</head>
<body>
<h1>Welcome to Warung Aisyah</h1>
<p>Our live menu lives at /api/menu.</p>
</body>
</html>Now add one line of middleware, above app.listen:
app.use(express.static('public'))Open http://localhost:3000/. Express finds public/index.html and shows the welcome page.
What changed? Your one server now does two jobs. / serves a ready-made page, while /api/menu serves live data — files and an API from the same app.
Try It Yourself
13 minKeep working in server.js. Restart with node server.js after each change, then check the browser.
Task 1 — Add a dish
Add a fourth item to the
menuarray — for example char kway teow at RM 6.00, with the nextid. Reload/api/menuand check it appears in the JSON.{ id: 4, name: 'Char kway teow', price: 6.0 }Task 2 — A count route
Add
GET /api/menu/countthat returns{ count: menu.length }as JSON. Place it above the/api/menu/:idroute, otherwise:idcatches the wordcountfirst.{ "count": 4 }Task 3 — Add an image
Drop any image into the
publicfolder — call itlogo.png. Openhttp://localhost:3000/logo.pngand watch Express serve it with no route of your own. Then link it fromindex.html.
Mini-Challenge · The Cheap Eats Route
8 minAdd a route GET /api/menu/cheap that returns only the dishes under RM 5. Reach for the array .filter method from Level 2, then send the result with res.json(...).
It works if: opening /api/menu/cheap shows a JSON array holding only the dishes priced below RM 5.
Reveal a sample answer
Put this route above /api/menu/:id, so the word cheap is not read as an id:
app.get('/api/menu/cheap', (req, res) => {
const cheap = menu.filter((dish) => dish.price < 5)
res.json(cheap)
})Opening /api/menu/cheap shows:
[
{ "id": 2, "name": "Roti canai", "price": 1.5 },
{ "id": 3, "name": "Teh tarik", "price": 2.5 }
]Nasi lemak at RM 5.50 is left out — its price is not below 5. Your filter did the choosing; res.json sent the result.
Recap
3 minres.json(...)sends an array or object back as JSON data.- Read a URL placeholder like
:idwithreq.params.id. - When nothing matches, reply with
res.status(404).json({ ... }). express.static('public')serves files with no route of your own.- One app can serve both static files and a live JSON API.
New words
- JSON API — a set of routes that return data as JSON text.
- Endpoint — one URL your API answers, such as
/api/menu. - express.static — middleware that serves files from a folder.
- Static file — a page or image sent unchanged, straight from disk.
Homework
4 min to brief · ~20 min to doRequired
- Build a small server with a 3-item JSON API of your own — a drinks list, a book list, anything. Return it from a route with
res.json(...). - Add a
publicfolder with one static page (index.html) served byexpress.static('public'). Take a screenshot of both — the JSON and the page — and bring them next lesson.
Optional stretch
- In two sentences, write the difference between an API route and a static file. Explain when you would reach for each.
- Add a
/api/items/:idroute to your homework API, with a404reply when the id is not found. Test both a hit and a miss.