Building an Experiment Data Server with Express
So far you've learned to request data from a server using fetch. Now we'll build the other side โ the server that sends the data.
Just as labs use LIMS (Laboratory Information Management System), web applications need a server that stores and retrieves data. Express is the tool for building that server. It's a web framework that runs on top of the Node.js you learned earlier โ think of it as a server development kit.
You can build a server with Node.js alone (remember http.createServer?), but you'd have to implement everything yourself. Express handles repetitive tasks like serving different data based on URLs, providing files, and handling errors. It's like using a validated kit instead of buying individual reagents and mixing them yourself.
Hello World: Starting a Server in 3 Lines
The most basic form of an Express server:
const express = require("express");
const app = express();
app.get("/", function(req, res) {
res.send("LIMS Server Running");
});
app.listen(3000, function() {
console.log("Server started: http://localhost:3000");
});Save this as server.js and run node server.js in the terminal. Then visit http://localhost:3000 in your browser.
Breaking down the key elements:
require("express")โ loads the Express libraryapp.get("/", ...)โ registers a function to run when someone visits "/"reqโ request. Information sent by the clientresโ response. Information the server will send backres.send()โ sends data to the clientapp.listen(3000)โ listens for requests on port 3000
Routing: Serving Different Data by URL
Routing is deciding "when a request comes to this URL, what data should we send back?" Like entering a sample ID in LIMS and getting that sample's info, or entering a protocol name and getting that protocol.
const express = require("express");
const app = express();
app.get("/", function(req, res) {
res.send("Welcome to the Experiment Data Server");
});
app.get("/genes", function(req, res) {
const genes = [
{ name: "TP53", chromosome: "17p13.1", type: "tumor suppressor" },
{ name: "BRCA1", chromosome: "17q21.31", type: "DNA repair" },
{ name: "EGFR", chromosome: "7p11.2", type: "receptor tyrosine kinase" }
];
res.json(genes);
});
app.get("/protocols", function(req, res) {
const protocols = ["DNA Extraction", "PCR", "Western Blot", "ELISA"];
res.json(protocols);
});
app.listen(3000, function() {
console.log("Server started: http://localhost:3000");
});Visit http://localhost:3000/genes and you'll see the gene list; visit /protocols and you'll see the protocol list as JSON. res.json() converts JavaScript objects to JSON format and sends them.
Dynamic Routing: Getting Parameters from URLs
To look up information for a specific gene, put a parameter in the URL:
const geneDatabase = {
TP53: { name: "TP53", chromosome: "17p13.1", function: "tumor suppressor", length_bp: 19149 },
BRCA1: { name: "BRCA1", chromosome: "17q21.31", function: "DNA repair", length_bp: 81189 },
EGFR: { name: "EGFR", chromosome: "7p11.2", function: "receptor tyrosine kinase", length_bp: 188307 }
};
app.get("/gene/:id", function(req, res) {
const geneId = req.params.id;
const gene = geneDatabase[geneId];
if (gene) {
res.json(gene);
} else {
res.status(404).json({ error: "Gene not found", query: geneId });
}
});Visit /gene/TP53 and you get TP53's info; visit /gene/XYZ123 and you get a 404 error. The :id part is a URL parameter โ a different value comes in with each request, and you extract it with req.params.id.
Middleware: Pre-processing Steps
Middleware is a core Express concept. It's a processing step that runs between when a request arrives at the server and when the response goes out.
Think of the pre-processing steps a sample goes through before entering an analysis instrument:
[Sample received] โ [Label check] โ [QC test] โ [Pre-treatment] โ [Analysis] โ [Report]In Express, requests go through a similar pipeline:
[Request arrives] โ [Middleware 1] โ [Middleware 2] โ [Route handler] โ [Response sent]Middleware is registered with app.use():
const express = require("express");
const app = express();
app.use(function(req, res, next) {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next();
});
app.get("/genes", function(req, res) {
res.json([{ name: "TP53" }, { name: "BRCA1" }]);
});
app.listen(3000);The function registered with app.use() runs on every request. Here it logs the time, method (GET/POST), and URL for every incoming request. Like noting "what time, which sample, which analysis" in your lab notebook.
The key is next(). You must call this function to proceed to the next step (next middleware or route handler). Without calling next(), the request stops here and no response is sent โ like a sample that fails QC and doesn't move to the next step.
Practical Middleware: Parsing POST Data
When a client sends data via POST (e.g., registering a new sample), you need to parse that data to read it. Express provides built-in middleware for this:
app.use(express.json());
app.post("/samples", function(req, res) {
const newSample = req.body;
console.log("New sample registered:", newSample);
res.json({ message: "Sample registration complete", sample: newSample });
});Without express.json(), req.body would be undefined. This middleware automatically parses incoming JSON data and puts it in req.body.
Full Example: Sample Management API
Let's combine everything learned so far to build a simple sample management server:
const express = require("express");
const app = express();
app.use(express.json());
app.use(function(req, res, next) {
console.log(`[LOG] ${req.method} ${req.url}`);
next();
});
const samples = [
{ id: "S001", name: "Blood Sample A", od: 1.85, status: "pass" },
{ id: "S002", name: "Tissue Sample B", od: 0.42, status: "fail" },
{ id: "S003", name: "Serum Sample C", od: 2.10, status: "pass" }
];
app.get("/samples", function(req, res) {
res.json(samples);
});
app.get("/sample/:id", function(req, res) {
const found = samples.find(function(s) {
return s.id === req.params.id;
});
if (found) {
res.json(found);
} else {
res.status(404).json({ error: "Sample not found" });
}
});
app.get("/samples/passed", function(req, res) {
const passed = samples.filter(function(s) {
return s.status === "pass";
});
res.json({ count: passed.length, samples: passed });
});
app.listen(3000, function() {
console.log("Sample management server started: http://localhost:3000");
});This server provides three API endpoints:
GET /samplesโ full sample listGET /sample/S001โ look up a specific sampleGET /samples/passedโ filter QC-passed samples only
Using fetch from the earlier topic to call this server:
fetch("http://localhost:3000/samples/passed")
.then(function(response) { return response.json(); })
.then(function(data) { console.log(data); });
// { count: 2, samples: [{ id: "S001", ... }, { id: "S003", ... }] }This is the moment frontend (fetch) and backend (Express) connect. It's a miniature version of what happens behind the scenes when you search for genes on NCBI.
Try It Yourself (Faded Example)
Fill in the blanks to complete an Express route that returns gene information.
const express = require("express");const app = express();app.get("/gene/", function(req, res) {const geneId = req..id;res.json({ gene: geneId, found: true });});app.listen();
Common Errors & Solutions
Q: Cannot GET /path error
No route is registered for that path. Check that app.get("/path", ...) exists and has no typos. Express automatically sends a Cannot GET message when no matching route is found.
Q: req.body is undefined
Make sure app.use(express.json()) is declared above your routes. Middleware executes in code order. If the JSON parsing middleware is after the route, the request reaches the route before parsing happens.
Q: Changes to the server aren't reflected
Node.js doesn't auto-reload when code changes. Stop the server with Ctrl+C in the terminal and restart with node server.js. For auto-restart, install nodemon and run nodemon server.js.
Q: Error: listen EADDRINUSE: address already in use :::3000
Another program is already using port 3000. A previously started server might still be running. Check with lsof -i :3000 and terminate it, or change the port number to 3001, etc.
Q: Express is called a "framework" โ how is it different from other npm packages (like mysql2)?
The key difference is who calls whom. mysql2 is a library โ you call connection.query() when you need it. Express is a framework โ you register your code with app.get("/path", callback), and when a request arrives, Express calls your callback.
This concept is called "Inversion of Control." For a more detailed analogy and explanation, see the Framework vs Library topic.