Mini Messageboard: A Complete Walkthrough of server.js
(Node.js + Express + MariaDB)
This page explains the entire JavaScript program that powers the Mini Messageboard web app. The goal is to understand how a request from a browser becomes database queries and HTML responses.
Reference material used
- Express routing: https://expressjs.com/en/guide/routing.html
- HTTP GET vs POST: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods
- HTML forms: https://www.w3schools.com/tags/tag_form.asp
- dotenv /
.env: https://www.npmjs.com/package/dotenv - SQL basics: https://www.w3schools.com/sql/
- SQL JOIN: https://www.w3schools.com/sql/sql_join.asp
- MariaDB Node connector: https://mariadb.com/docs/connectors/mariadb-connector-nodejs/
- XSS prevention: https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
The Setup
import express from 'express';
import 'dotenv/config';
import { query } from './db.js';
const app = express();
app.use(express.urlencoded({ extended: false }));
const port = process.env.PORT;
if (!port) {
throw new Error('PORT not set. Create .env (cp .env.example .env) and set PORT=41xx');
}What this section does
- Imports Express for routing
- Loads environment variables using dotenv
- Imports a helper function
query()that runs SQL using MariaDB - Creates the Express app
- Enables parsing of HTML form submissions
- Reads the port from the environment and fails early if it is missing
Why fail early?
On a shared server, every student must use a different port. If the port is wrong or missing, the program should stop immediately.
Route 1: GET / (Home Page)
app.get('/', async (req, res) => {
const users = await query('SELECT id, name FROM users ORDER BY name');
const selected = Number(req.query.user_id || 0);
res.send(`
<h1>Mini Messageboard</h1>
<p><b>No login:</b> select who you are, then view/post messages.</p>
<form method="GET" action="/messages">
<label for="user_id"><b>Who are you?</b></label>
<select name="user_id" id="user_id" required>
<option value="" ${selected ? '' : 'selected'} disabled>Select a user...</option>
${users.map(u => `<option value="${u.id}" ${u.id === selected ? 'selected' : ''}>${u.name}</option>`).join('')}
</select>
<button type="submit">Continue</button>
</form>
<hr/>
<p>Tip: If you refresh and lose your selection, just pick yourself again.</p>
`);
});
What’s happening here
- Express routing
This handler runs when the browser requests/. - Database query
SELECT id, name FROM users ORDER BY nameThis pulls user data from MariaDB so the UI reflects the database, not hard-coded values. - Query parameters
req.query.user_idcomes from URLs like:/?user_id=2 - Dynamic HTML generation
The<select>dropdown is built by mapping database rows into<option>elements.
HTML Generated by the Home Page
<pre data-enlighter-language=”html”> <form method=”GET” action=”/messages”> <label for=”user_id”><b>Who are you?</b></label> <select name=”user_id” id=”user_id” required> <option value=”” disabled>Select a user…</option> <option value=”1″>Alice</option> <option value=”2″>Bob</option> </select> <button type=”submit”>Continue</button> </form> </pre>
Submitting this form creates a URL like:
/messages?user_id=1
This is a GET request carrying data in the query string.
Route 2: GET /messages (View + Post Messages)
app.get('/', async (req, res) => {
const users = await query('SELECT id, name FROM users ORDER BY name');
const selected = Number(req.query.user_id || 0);
res.send(`
<h1>Mini Messageboard</h1>
<p><b>No login:</b> select who you are, then view/post messages.</p>
<form method="GET" action="/messages">
<label for="user_id"><b>Who are you?</b></label>
<select name="user_id" id="user_id" required>
<option value="" ${selected ? '' : 'selected'} disabled>Select a user...</option>
${users.map(u => `<option value="${u.id}" ${u.id === selected ? 'selected' : ''}>${u.name}</option>`).join('')}
</select>
<button type="submit">Continue</button>
</form>
<hr/>
<p>Tip: If you refresh and lose your selection, just pick yourself again.</p>
`);
});
Key ideas here
- Validation: redirect if
user_idis missing - Prepared statements (
WHERE id=?) keep SQL separate from data - SQL JOIN combines users and posts correctly
- The server generates all HTML — no client JavaScript required
Route 3: POST /messages (Insert a Message)
app.post('/messages', async (req, res) => {
const userId = Number(req.body.user_id || 0);
const content = String(req.body.content || '').trim();
if (!userId || !content) return res.redirect('/');
if (content.length > 500) {
return res.status(400).send('Message too long (max 500 chars).');
}
await query('INSERT INTO posts (user_id, content) VALUES (?, ?)', [userId, content]);
res.redirect(`/messages?user_id=${userId}`);
});
Why this pattern matters
- POST modifies data
- Redirect after POST prevents duplicate submissions on refresh
- Parameterized SQL prevents injection bugs
Helper: Escaping Output (XSS Prevention)
function escapeHtml(s) {
return String(s)
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''');
}
Any time user input is displayed in HTML, it must be escaped. This prevents malicious users from injecting scripts into the page.
Helper: Formatting Timestamps
function formatTime(ts) {
try {
const d = (ts instanceof Date) ? ts : new Date(ts);
if (Number.isNaN(d.getTime())) return '';
return d.toLocaleString();
} catch {
return '';
}
}
Starting the Server
app.listen(Number(port), () => {
console.log(`Server running on port ${port}`);
});
>
If everything is configured correctly, you’ll see:
Server running on port 4178
What This Program Teaches
- Express routing and request handling
- GET vs POST semantics
- HTML form mechanics
- SQL queries and JOINs
- Safe database access from Node
- Why escaping output matters
- Clean separation of configuration, logic, and data
