The plain app
First the shop itself, with no telemetry at all. You will add the instrumentation to these two files later, so you see exactly what it changes.
/api/products · /api/checkout · /api/fail
Step 0. Check your tools
Section titled “Step 0. Check your tools”Node 24 or newer:
node -vDocker Desktop must be running. This prints the server details, not “Cannot connect”:
docker infoVS Code’s code command. If it is not found, open VS Code, press Cmd+Shift+P and run Shell Command: Install ‘code’ command in PATH.
code --versionStep 1. Create the folders and open VS Code
Section titled “Step 1. Create the folders and open VS Code”From the folder where you keep your projects:
mkdir -p tiny-shop-otel/backend tiny-shop-otel/stackcd tiny-shop-otelcode .In VS Code, open a terminal with Ctrl+`. You will use three terminal tabs (the + button): one for the backend, one for the frontend, one for the stack. Each new tab opens in tiny-shop-otel/.
Step 2. A simple API, no telemetry yet
Section titled “Step 2. A simple API, no telemetry yet”In the first terminal tab, from tiny-shop-otel/:
cd backendnpm init -ynpm install express corsCreate backend/server.js:
const express = require("express");const cors = require("cors");
const app = express();app.use(cors());app.use(express.json());
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));const products = [ { id: 1, name: "Coffee", price: 4 }, { id: 2, name: "Tea", price: 3 }, { id: 3, name: "Cake", price: 6 },];
app.get("/api/products", async (req, res) => { await sleep(80); res.json(products);});
app.post("/api/checkout", async (req, res) => { await sleep(150); res.json({ ok: true, orderId: Math.floor(Math.random() * 100000) });});
app.get("/api/fail", (req, res) => { res.status(500).json({ error: "inventory service unavailable" });});
app.listen(5001, () => console.log("tiny-shop API on http://localhost:5001"));Three routes: the product list, a checkout that takes 150 ms, and /api/fail, which always answers 500. You will use that one to make the session errored.
Run it:
node server.jsIn another terminal tab, check that it answers:
curl localhost:5001/api/productsYou get the three products as JSON. Leave the API running.
Step 3. A dummy React UI
Section titled “Step 3. A dummy React UI”In the second terminal tab, from tiny-shop-otel/ (Vite creates frontend/):
npm create vite@latest frontend -- --template react --no-interactivecd frontendnpm installReplace frontend/src/App.jsx with:
import { useEffect, useState } from "react";
const API = "http://localhost:5001";
export default function App() { const [products, setProducts] = useState([]); const [cart, setCart] = useState([]); const [message, setMessage] = useState("");
useEffect(() => { fetch(`${API}/api/products`).then((r) => r.json()).then(setProducts); }, []);
async function checkout() { const res = await fetch(`${API}/api/checkout`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ items: cart.map((p) => p.id) }), }); const data = await res.json(); setMessage(`Order #${data.orderId} placed`); setCart([]); }
async function callBrokenApi() { const res = await fetch(`${API}/api/fail`); setMessage(`API answered ${res.status}`); }
function crash() { throw new Error("Button exploded"); }
return ( <main style={{ fontFamily: "sans-serif", maxWidth: 480, margin: "40px auto" }}> <h1>Tiny Shop</h1> <ul> {products.map((p) => ( <li key={p.id}> {p.name} (${p.price}) <button onClick={() => setCart([...cart, p])}>Add</button> </li> ))} </ul> <p>Cart: {cart.length} item(s)</p> <button onClick={checkout} disabled={!cart.length}>Checkout</button>{" "} <button onClick={callBrokenApi}>Call broken API</button>{" "} <button onClick={crash}>Throw error</button> <p>{message}</p> </main> );}Start it:
npm run devWhat you should see
Section titled “What you should see”Open http://localhost:5173. You see Tiny Shop and three products: Coffee, Tea and Cake. They came from your API, so the page reached it.
Click Add, then Checkout: the page says “Order #… placed”. Call broken API says “API answered 500”, and Throw error does nothing you can see, apart from an error in the DevTools Console.
The app works, and nothing is instrumented yet. Leave both apps running.