Tangy is building your next step
Shaping the lesson around what you want to make.
Tangy is building your next step
Shaping the lesson around what you want to make.
Computer Science
You will build a small Node.js web app that retrieves product data, then measure and improve it using browser/HTTP caching, server-side caching, database-style caching, and in-memory caching. Each step leaves the app faster or more observable, so you finish with a project you can run and demonstrate.
Follow the path module by module. The layout keeps the lesson count, your progress, and the module description in one scan.
See the Cost of Repeated Work
2 lessonsCreate a small web app whose repeated product lookups are visibly slower than necessary. The learner first gets a working baseline and simple measurements before adding any cache.
Cache at the Browser and HTTP Boundary
2 lessonsUse HTTP semantics to let a client reuse a response instead of downloading unchanged data every time. The learner verifies the behavior through observable headers and conditional requests.
Cache Work Inside the Server
3 lessonsMove caching into the application so repeated requests avoid the slow data-loading step even when the client asks for the resource again. The learner builds a small, understandable in-memory cache rather than hiding the behavior behind a service.
Choose and Prove a Caching Strategy
3 lessonsConnect the layers into a practical design and finish with a visible comparison. The learner makes caching decisions based on freshness and workload rather than adding cache code blindly.
Public lesson
A product app has two separate jobs:
For this first version, keep both parts deliberately small: the data stays in a local JSON file, and a Node.js HTTP server returns either the whole list or one product by ID. The important detail is that the server should not return the same response for every URL. It needs to inspect the request path and make a decision.
Tasks
Open the existing project and identify:
Do not add setup or scaffolding. Use the project’s existing structure and start command.
If the project does not yet contain product data, add a small list to its existing local JSON file. Use a shape like this, adapting the fields to the project:
[
{
"id": "1",
"name": "Notebook",
"price": 8.5
},
{
"id": "2",
"name": "Desk lamp",
"price": 24
}
]
[
{
"id": "1",
"name": "Notebook",
"price": 8.5
},
{
"id": "2",
"name": "Desk lamp",
"price": 24
}
]
Keep the IDs as strings. A URL such as /products/2 gives the server "2" as part of the path, so comparing strings avoids an unnecessary type conversion.
In the existing server entry file, begin with this incomplete shape. Replace the JSON import path with the actual path used by the project:
const http = require("node:http");
const products = require("./REPLACE_WITH_THE_LOCAL_JSON_PATH");
const server = http.createServer((request, response) => {
const url = new URL(request.url, `http://${request.headers.host}`);
const pathParts = url.pathname.split("/").filter(Boolean);
response.setHeader("Content-Type", "application/json");
// TODO:
// - return the complete product list for the collection route
// - return one product for a route containing an ID
// - return a 404 response for anything else
});
server.listen(/* use the project's existing port convention */);
const http = require("node:http");
const products = require("./REPLACE_WITH_THE_LOCAL_JSON_PATH");
const server = http.createServer((request, response) => {
const url = new URL(request.url, `http://${request.headers.host}`);
const pathParts = url.pathname.split("/").filter(Boolean);
response.setHeader("Content-Type", "application/json");
// TODO:
// - return the complete product list for the collection route
// - return one product for a route containing an ID
// - return a 404 response for anything else
});
server.listen(/* use the project's existing port convention */);
The URL object gives you a reliable pathname. Splitting that pathname turns:
/products/2
/products/2
into:
["products", "2"]
["products", "2"]
That makes the two routes easy to distinguish:
["products"] means “return the collection”["products", id] means “find one product”Tasks
Now complete the request handling. Your code should:
GET requests404 when the route or product does not existJSON.stringify(...)A useful incomplete decision block to adapt is:
if (request.method !== "GET") {
// send a method-not-allowed response
} else if (pathParts.length === 1 && pathParts[0] === "products") {
// send the complete products array
} else if (pathParts.length === 2 && pathParts[0] === "products") {
const id = pathParts[1];
// Find the product whose id matches `id`.
// Then send it, or send a 404 if there is no match.
} else {
// send a 404 response
}
if (request.method !== "GET") {
// send a method-not-allowed response
} else if (pathParts.length === 1 && pathParts[0] === "products") {
// send the complete products array
} else if (pathParts.length === 2 && pathParts[0] === "products") {
const id = pathParts[1];
// Find the product whose id matches `id`.
// Then send it, or send a 404 if there is no match.
} else {
// send a 404 response
}
For each response, remember that a response needs both a status and a body. For example, the general pattern is:
response.statusCode = 200;
response.end(JSON.stringify(value));
response.statusCode = 200;
response.end(JSON.stringify(value));
For a missing product, use a non-success status and a small JSON error object rather than returning an empty success response. That distinction lets a browser, test, or future client tell “there are no matching products” apart from “the request succeeded.”
Tasks
Run the project using its documented start command.
Request the collection route in a browser or with the project’s usual request tool:
/products
/products
You should see the JSON array from the local file.
Then request a product that exists:
/products/2
/products/2
You should see one object rather than the whole array.
Finally, request an ID that is not in the file:
/products/does-not-exist
/products/does-not-exist
This should produce a 404 response and a JSON error body. Also try a path such as:
/not-a-product-route
/not-a-product-route
It should not accidentally return the product list.
Without the route check, the server could load the data, but it would not yet be an API: every request would receive the same thing. The small pathParts decision creates two useful meanings from one data source:
/products asks for the collection/products/:id asks for a member of that collectionThe local JSON file remains the source of truth, while the HTTP server becomes the boundary that presents that data to a browser or another program.
Tasks
After the checks pass, change one product’s name or price in the JSON file, restart the app if the project caches the imported file, and request the product again. The response should reflect the local file rather than a hard-coded value in the server. That is the baseline product app working end to end: local data is loaded, a request selects the right product view, and the result is observable over HTTP.
4 modules · 10 lessons
See the Cost of Repeated Work
Cache at the Browser and HTTP Boundary
Cache Work Inside the Server
Choose and Prove a Caching Strategy
Learn by building your own version.
Remix this public project to open the workspace, follow the guided build, and let the AI mentor teach you through the work instead of doing it for you.