Frontend Implementation, Part 1: the Pages, the Stylesheet and Talking to the Server
Chapter Forty
Syllabus topic Module 2, "Application Development: Frontend implementation", first part: the pages, the stylesheet and the module that calls the API.
Pages 259 to 269 of 499
In one line
The frontend is what a student, the counter and the owner actually touch: a handful of HTML pages that say what is on each screen, one stylesheet that says how it looks on a phone first and on a wide screen second, and one small module through which every page talks to the server, sending and receiving JSON and turning every refusal into a message the page can show.
In the wording to use when asked: frontend implementation turns the screen design into markup, styles and scripts: semantic HTML for structure and accessibility, CSS for presentation with a mobile-first responsive layout, and client-side JavaScript that calls the backend's API through a single request layer which serialises JSON, carries the session, and maps error responses to a consistent client-side error type.
What the frontend is made of
Everything the browser downloads lives in the project's public/ folder, and the same Express server that answers the API hands it out (Chapter 28):
| File | What it is |
|---|---|
index.html | signing in and registering |
menu.html | today's menu and the order being put together (printed in Chapter 27) |
orders.html | the student's orders today |
counter.html | the counter's list, and what the kitchen still has to make |
owner.html | the menu, today's stock, the day's report, staff accounts |
404.html | the page for an address that does not exist |
favicon.svg | the small picture in the browser's tab |
css/app.css | the one stylesheet |
js/api.js | the only code that talks to the server |
js/mock.js | a pretend server, for building pages before the real one answered |
js/ui.js and one script per page | Chapter 41 |
The pages
Every page has the same anatomy: the same <head>, a bar across the top with the product's name and, once someone is signed in, the navigation and a Sign out button; a <main> holding one heading, a message area, and the page's own content; and one script, loaded as a JavaScript module. The sign-in page:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sign in | Canteen Pre-order</title>
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="/css/app.css">
<script type="module" src="/js/signin.js"></script>
</head>
<body>
<header class="bar">
<span class="brand">Canteen Pre-order</span>
</header>
<main class="page narrow">
<h1>Order lunch before the break</h1>
<p class="lead">Choose your food and a pickup time. Collect
it from the counter and pay there.</p>
<p class="message" id="message" role="alert" hidden></p>
<form id="signin-form" class="card">
<h2>Sign in</h2>
<label for="signin-email">College email</label>
<input id="signin-email" name="email" type="email"
autocomplete="username" required>
<label for="signin-password">Password</label>
<input id="signin-password" name="password" type="password"
autocomplete="current-password" required>
<button type="submit">Sign in</button>
</form>
<form id="register-form" class="card">
<h2>New here? Create an account</h2>
<label for="register-name">Your name</label>
<input id="register-name" name="name" autocomplete="name"
required minlength="2" maxlength="80">
<label for="register-email">College email</label>
<input id="register-email" name="email" type="email"
autocomplete="email" required maxlength="120">
<label for="register-password">Password: 8 or more
characters, and not one you use anywhere else</label>
<input id="register-password" name="password" type="password"
autocomplete="new-password" required minlength="8"
maxlength="128">
<button type="submit">Create account</button>
</form>
</main>
</body>
</html>Frontend Implementation, Part 1: the Pages, the Stylesheet and Talking to the Server
What each part is for:
<meta name="viewport">tells a phone to lay the page out at its own width. Without it, a phone pretends to be a desktop and shrinks the page to an unreadable size.<script type="module">loads the page's script as a module: it canimportthe shared modules, and it runs after the page has been read, so it can find every element.- Every field has a
<label>tied to it byforandid(NFR-8): tapping the label focuses the field, and a screen reader says what the field is for. autocompletetells the browser and password managers what each field holds:usernameandcurrent-passwordto fill in a saved sign-in,new-passwordto offer to make a strong one. OWASP's verification standard asks that password managers are not blocked, and these values are what let them work.required,minlengthandmaxlengthrepeat the server's own rules, so a mistake is caught before the request is sent. They are for the user's convenience only: the server checks everything again (Chapter 47).role="alert"on the message area makes a screen reader read a message the moment it appears. It startshiddenand the script shows it.- The password label carries the advice security control S5 asks for, a password used nowhere else, in the label itself so it is read whenever the field is (Chapter 46).
The student's orders page is the shortest:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My orders | Canteen Pre-order</title>
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="/css/app.css">
<script type="module" src="/js/orders.js"></script>
</head>
<body>
<header class="bar">
<span class="brand">Canteen Pre-order</span>
<nav>
<a href="/menu.html">Menu</a>
<a href="/orders.html" aria-current="page">My orders</a>
</nav>
<span class="who" id="who"></span>
<button type="button" class="link" id="signout">Sign out</button>
</header>
<main class="page narrow">
<h1>My orders today</h1>
<p class="hint">This page updates itself every 15 seconds.</p>
<p class="message" id="message" role="alert" hidden></p>
<div id="orders" aria-live="polite">
<p>Loading your orders...</p>
</div>
</main>
</body>
</html>aria-current="page" marks the link to the page the user is on, for screen readers and for the stylesheet. aria-live="polite" makes a screen reader announce the orders when they change, without interrupting what it is reading. The page says in words that it updates itself (FR-12), so nobody waits for a button that is not there.
The counter's page puts the list of orders beside what the kitchen still has to make:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Counter | Canteen Pre-order</title>
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="/css/app.css">
<script type="module" src="/js/counter.js"></script>
</head>
<body>
<header class="bar">
<span class="brand">Canteen Pre-order</span>
<nav>
<a href="/counter.html" aria-current="page">Counter</a>
<a href="/owner.html" id="owner-link" hidden>Owner</a>
</nav>
<span class="who" id="who"></span>
<button type="button" class="link" id="signout">Sign out</button>
</header>
<main class="page with-side">
<section class="main-column">
<h1>Orders at the counter</h1>
<label for="slot">Pickup slot</label>
<select id="slot">
<option value="">All slots</option>
</select>
<p class="message" id="message" role="alert" hidden></p>
<div id="orders" aria-live="polite">
<p>Loading orders...</p>
</div>
</section>
<section class="card side-column">
<h2>Still to make</h2>
<p class="hint" id="kitchen-slot">Choose a slot to see
what the kitchen still has to make for it.</p>
<table id="kitchen" hidden>
<thead><tr><th>Item</th><th>Quantity</th></tr></thead>
<tbody></tbody>
</table>
</section>
</main>
</body>
</html>Frontend Implementation, Part 1: the Pages, the Stylesheet and Talking to the Server
The Owner link starts hidden: the owner is a kind of counter staff (Chapter 20) and uses this page too, and the script shows the link only when the owner is signed in. Hiding a link is a convenience, not security: the owner's page and its API refuse anyone else (Chapter 46). The kitchen list is a real <table>, because it is tabular data, with a header row a screen reader can announce.
The owner's page is the longest, because the owner does the most kinds of thing:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Owner | Canteen Pre-order</title>
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="/css/app.css">
<script type="module" src="/js/owner.js"></script>
</head>
<body>
<header class="bar">
<span class="brand">Canteen Pre-order</span>
<nav>
<a href="/counter.html">Counter</a>
<a href="/owner.html" aria-current="page">Owner</a>
</nav>
<span class="who" id="who"></span>
<button type="button" class="link" id="signout">Sign out</button>
</header>
<main class="page">
<h1>The canteen today</h1>
<p class="message" id="message" role="alert" hidden></p>
<section>
<h2>Menu and today's stock</h2>
<p class="hint">Set each item's stock every morning.</p>
<div id="menu-list"><p>Loading the menu...</p></div>
</section>
<form id="add-item" class="card">
<h2>Add an item</h2>
<label for="item-name">Name</label>
<input id="item-name" name="name" required minlength="2"
maxlength="60">
<label for="item-category">Category</label>
<select id="item-category" name="category" required>
<option value="meals">Meals</option>
<option value="snacks">Snacks</option>
<option value="drinks">Drinks</option>
<option value="desserts">Desserts</option>
</select>
<label for="item-price">Price in rupees</label>
<input id="item-price" name="price" type="number" min="1"
max="1000" step="0.5" required>
<label class="check"><input type="checkbox" name="isVeg"
checked> Vegetarian</label>
<button type="submit">Add item</button>
</form>
<section class="card">
<h2>Today's report</h2>
<div id="report"><p>Loading the report...</p></div>
</section>
<form id="add-staff" class="card">
<h2>Add a counter staff account</h2>
<label for="staff-name">Name</label>
<input id="staff-name" name="name" required minlength="2"
maxlength="80">
<label for="staff-email">Email</label>
<input id="staff-email" name="email" type="email" required>
<label for="staff-password">Password, 8 or more
characters</label>
<input id="staff-password" name="password" type="password"
autocomplete="new-password" required minlength="8"
maxlength="128">
<button type="submit">Add account</button>
</form>
</main>
</body>
</html>The owner types a price in rupees, as people think of prices, with step="0.5" allowing half-rupees; the script turns it into paise before sending it, because the API and the database hold only whole paise (ADR-2). The checkbox sits inside its label, which is a second correct way to tie a label to a field.
The last page is the one nobody should need, served for any address that does not exist:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Page not found | Canteen Pre-order</title>
<link rel="stylesheet" href="/css/app.css">
</head>
<body>
<main class="page narrow">
<h1>Page not found</h1>
<p>There is no page at this address.</p>
<p><a href="/">Go to the sign-in page</a></p>
</main>
</body>
</html>Frontend Implementation, Part 1: the Pages, the Stylesheet and Talking to the Server
It has no script: a page shown because something went wrong should depend on as little as possible. And the icon in the browser's tab is five lines of SVG, a bowl on the brand colour, drawn as text so that it is sharp at every size:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="6" fill="#9a3412"/>
<path d="M9 19h14a7 7 0 0 1-14 0z" fill="#fff"/>
<path d="M8 17h16" stroke="#fff" stroke-width="2"/>
</svg>The stylesheet
One stylesheet serves every page. It is written for a phone first: everything outside the one media query at the end is the phone's layout, and the media query only adds a second column when the screen is at least 48rem wide. The whole file:
/* Canteen Pre-order: one stylesheet for every page.
Written for a phone first; the rules inside the media
query at the end only add a second column on a wide
screen. Colours are variables, so they change in one
place, and every text colour meets a contrast of at
least 4.5 to 1 against the background it sits on. */
:root {
--ink: #1c1917;
--muted: #57534e;
--paper: #ffffff;
--surface: #f5f1ea;
--line: #d6d3d1;
--brand: #9a3412;
--brand-dark: #7c2d12;
--veg: #166534;
--nonveg: #9f1239;
--error: #b91c1c;
--ok: #166534;
--radius: 8px;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: system-ui, -apple-system, "Segoe UI", Roboto,
sans-serif;
font-size: 1rem;
line-height: 1.5;
color: var(--ink);
background: var(--surface);
}
h1 {
font-size: 1.5rem;
margin: 0.5rem 0 1rem;
}
h2 {
font-size: 1.15rem;
margin: 0 0 0.75rem;
}
a {
color: var(--brand-dark);
}
/* The bar across the top of every page. */
.bar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem 1rem;
padding: 0.75rem 1rem;
background: var(--brand);
color: #fff;
}
.bar .brand {
font-weight: 700;
margin-right: auto;
}
.bar nav {
display: flex;
gap: 1rem;
}
.bar a,
.bar .link {
color: #fff;
}
.bar a[aria-current="page"] {
font-weight: 700;
text-decoration-thickness: 2px;
}
.who {
font-size: 0.9rem;
}
.page {
max-width: 64rem;
margin: 0 auto;
padding: 1rem;
}
.page.narrow {
max-width: 34rem;
}
/* The counter's slot chooser, above the list of orders. */
.main-column > select {
margin-bottom: 1rem;
}
.lead {
font-size: 1.1rem;
}
.hint {
color: var(--muted);
font-size: 0.9rem;
}
.card {
background: var(--paper);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 1rem;
margin-bottom: 1rem;
}
/* Forms: one field under another, big enough to tap. */
label {
display: block;
font-weight: 600;
margin: 0.75rem 0 0.25rem;
}
label.check {
font-weight: 400;
}
input,
select,
button {
font: inherit;
}
input:not([type="checkbox"]),
select {
width: 100%;
padding: 0.6rem;
border: 1px solid #78716c;
border-radius: var(--radius);
background: var(--paper);
}
button {
cursor: pointer;
border: 0;
border-radius: var(--radius);
padding: 0.65rem 1.1rem;
background: var(--brand);
color: #fff;
font-weight: 600;
}
form > button {
margin-top: 1rem;
width: 100%;
}
button:hover {
background: var(--brand-dark);
}
button:disabled {
background: #a8a29e;
color: var(--ink);
cursor: not-allowed;
}
button.link {
background: none;
padding: 0;
text-decoration: underline;
font-weight: 400;
}
button.quiet {
background: var(--paper);
color: var(--brand-dark);
border: 1px solid var(--brand-dark);
}
:focus-visible {
outline: 3px solid #2563eb;
outline-offset: 2px;
}
/* Messages to the user, and errors beside a field. */
.message {
padding: 0.75rem 1rem;
border-radius: var(--radius);
background: #fee2e2;
color: #7f1d1d;
}
.message.ok {
background: #dcfce7;
color: #14532d;
}
.field-error {
color: var(--error);
font-size: 0.9rem;
margin: 0.25rem 0 0;
}
[aria-invalid="true"] {
border-color: var(--error);
border-width: 2px;
}
/* The menu: one row per item. */
.category {
margin-bottom: 1.5rem;
}
.item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
background: var(--paper);
border: 1px solid var(--line);
border-radius: var(--radius);
margin-bottom: 0.5rem;
}
.item .about {
flex: 1;
}
.item .name {
font-weight: 600;
}
.item .note {
color: var(--muted);
font-size: 0.9rem;
}
.item.off .name {
color: var(--muted);
}
.stepper {
display: flex;
align-items: center;
gap: 0.5rem;
}
.stepper button {
width: 2.75rem;
height: 2.75rem;
padding: 0;
font-size: 1.25rem;
}
.stepper output {
min-width: 1.5rem;
text-align: center;
font-weight: 700;
}
/* Small labels: vegetarian or not, and an order's status. */
.badge {
display: inline-block;
padding: 0.05rem 0.5rem;
border-radius: 999px;
font-size: 0.8rem;
font-weight: 600;
color: #fff;
background: var(--muted);
}
.badge.veg {
background: var(--veg);
}
.badge.nonveg {
background: var(--nonveg);
}
.badge.placed {
background: #1d4ed8;
}
.badge.preparing {
background: #6b21a8;
}
.badge.ready {
background: var(--ok);
}
.badge.no_show {
background: var(--nonveg);
}
.lines {
list-style: none;
padding: 0;
margin: 0;
}
.lines li {
display: flex;
justify-content: space-between;
gap: 1rem;
padding: 0.25rem 0;
border-bottom: 1px dashed var(--line);
}
.total {
display: flex;
justify-content: space-between;
font-size: 1.1rem;
}
/* The order number, which the counter calls out across the
servery, is larger than the name beside it. */
.order .number {
font-size: 1.5rem;
font-weight: 700;
margin-right: 0.25rem;
}
.order .top {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 0.5rem;
}
.order .actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.75rem;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
text-align: left;
padding: 0.5rem;
border-bottom: 1px solid var(--line);
}
h3 {
font-size: 1rem;
margin: 0 0 0.5rem;
}
/* The owner's menu cards: the fields sit side by side and
wrap on to a second line on a narrow phone. */
.fields {
display: flex;
flex-wrap: wrap;
align-items: end;
gap: 0.75rem 1rem;
}
.fields label {
display: flex;
flex-direction: column;
gap: 0.25rem;
margin: 0;
}
.fields label.check {
flex-direction: row;
align-items: center;
gap: 0.4rem;
min-height: 2.75rem;
}
.fields input[type="number"] {
width: 6rem;
}
.scroll {
overflow-x: auto;
}
/* On a phone: the cart's summary, fixed to the foot of the
screen, taking the student down to the order form. */
.cart-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 0.9rem 1rem;
background: var(--brand-dark);
color: #fff;
font-weight: 700;
text-align: center;
}
.page.with-side {
padding-bottom: 4.5rem;
}
/* A wide screen: the order (or the kitchen list) sits in a
second column beside the main one, and stays in view. */
@media (min-width: 48rem) {
.page.with-side {
display: grid;
grid-template-columns: 1fr 20rem;
gap: 1.5rem;
align-items: start;
}
.side-column {
position: sticky;
top: 1rem;
}
.cart-bar {
display: none;
}
}Frontend Implementation, Part 1: the Pages, the Stylesheet and Talking to the Server
Read it in its sections:
Frontend Implementation, Part 1: the Pages, the Stylesheet and Talking to the Server
- The colours are variables in
:root, named for their job,--ink,--muted,--error, not for their hue. A colour changes in one place, and the contrast of every text colour against its background was measured at 4.5 to 1 or more (NFR-8); the weakest, the error text, is 6.47 to 1. box-sizing: border-boxmakes a width include the padding and the border, so a field set towidth: 100%never spills out of its card.- The system font stack uses the font each device already has: nothing is downloaded, and text appears at once on a slow connection.
- Sizes are in
rem, relative to the user's own text size, so a student who has made the phone's text larger gets a larger page, not a broken one. - Forms are one field under another, every field the full width of its card: the easiest layout to use with one thumb. The stepper's plus and minus buttons are 2.75rem square, 44 CSS pixels at the normal text size, well above NFR-8's 24.
:focus-visibledraws a thick blue outline around whatever the keyboard has reached, and only when the keyboard is in use, so every action can be followed without a mouse (NFR-8).- The status badges' class names are the statuses themselves,
placed,preparing,ready,no_show, so the script sets the class straight from the data (Chapter 41). Each badge also says its status in words: colour never carries a meaning alone. - The one media query,
min-width: 48rem, is the only rule that knows about wide screens. Below it, the order form sits under the menu and a bar fixed to the foot of the screen takes the student to it; above it, the form moves into a second column beside the menu and stays in view, and the bar disappears.
Talking to the server
Every request a page makes goes through one function. The whole module:
// Every request a page makes to the server goes through
// api(). It sends and reads JSON, keeps the sign-in cookie,
// and turns any failure into an ApiError the page can show.
//
// Open any page with ?mock=1 on the end of its address and
// the requests are answered by mock.js instead, with made-up
// data. That is how the pages were built and tried before
// the server existed.
import { mockFetch } from './mock.js';
const useMock = new URLSearchParams(location.search).has('mock');
export class ApiError extends Error {
constructor(status, body) {
const error = body && body.error ? body.error : {};
super(error.message || `The server answered ${status}.`);
this.status = status;
this.code = error.code;
this.details = error.details || {};
}
}
export async function api(method, path, body) {
const options = { method, headers: {} };
if (method !== 'GET') {
options.headers['Content-Type'] = 'application/json';
options.body = JSON.stringify(body ?? {});
}
let res;
try {
res = useMock
? await mockFetch(method, path, body)
: await fetch(path, options);
} catch {
throw new ApiError(0, { error: {
message: 'Cannot reach the canteen server. Check your '
+ 'internet connection and try again.',
} });
}
if (res.status === 204) return null;
const data = await res.json().catch(() => null);
if (!res.ok) throw new ApiError(res.status, data);
return data;
}Frontend Implementation, Part 1: the Pages, the Stylesheet and Talking to the Server
Forty-five lines carry five decisions:
fetchis the browser's own way to make a request; no library is needed. A page calls, for example,await api('POST', '/api/orders', { slot, items })and receives the server's JSON as an object.- The sign-in cookie travels by itself. The pages and the API come from the same server, and
fetchsends that server's cookies with every same-site request. No script ever sees the cookie, which isHttpOnly(Chapter 46). - Every request that changes something is sent as JSON, with its
Content-Typesaying so. The server refuses any change that is not JSON (Chapter 31, S7), which an ordinary form on another website cannot send. - Every refusal becomes one kind of error.
ApiErrorcarries the status, the error'scodefor the program, itsmessagefor people, and itsdetails, the problem with each field. One shape of error from the server (Chapter 30) means one way of showing it on every page (Chapter 41). - A failure to reach the server at all is also an
ApiError, with status 0 and a message in plain words, because on a phone at 12:15 the most common failure is the Wi-Fi, not the server.
A successful answer with nothing in it, 204, returns null; anything else is read as JSON. res.json().catch(() => null) keeps a broken or empty body from crashing the page: the status still says what happened.
The mock
The pages were started on 11 September, the same day as the server. For the student's pages, the team did not wait: api.js switches to a pretend server when the page's address ends in ?mock=1.
Frontend Implementation, Part 1: the Pages, the Stylesheet and Talking to the Server
// A pretend server, for building the pages before the real
// one existed (see api.js). It answers the same addresses
// with the same shapes of JSON the API specification sets
// out, from data kept in memory, so a page that works here
// works against the real server too.
const user = { id: 3, name: 'Priya Menon',
email: 'priya@college.example', role: 'student' };
const items = [
{ id: 1, name: 'Veg Thali', category: 'meals', pricePaise: 7000,
isVeg: true, isAvailable: true, stockLeft: 60 },
{ id: 3, name: 'Chicken Biryani', category: 'meals',
pricePaise: 11000, isVeg: false, isAvailable: true,
stockLeft: 4 },
{ id: 6, name: 'Vada Pav', category: 'snacks', pricePaise: 2000,
isVeg: true, isAvailable: true, stockLeft: 0 },
{ id: 11, name: 'Cutting Chai', category: 'drinks',
pricePaise: 1200, isVeg: true, isAvailable: true,
stockLeft: 150 },
];
const slots = ['12:30', '12:40', '12:50', '13:00'].map(
(time, i) => ({ time, cutoff: `12:${15 + i * 10}`, open: true }));
const orders = [];
function reply(status, data) {
return new Response(data === undefined ? null
: JSON.stringify(data), {
status, headers: { 'Content-Type': 'application/json' },
});
}
export async function mockFetch(method, path, body) {
const route = `${method} ${path.split('?')[0]}`;
if (route === 'GET /api/auth/me') return reply(200, { user });
if (route === 'POST /api/auth/logout') return reply(204);
if (route === 'GET /api/menu') return reply(200, { items });
if (route === 'GET /api/slots') return reply(200, { slots });
if (route === 'GET /api/orders/mine') {
return reply(200, { orders });
}
if (route === 'POST /api/orders') {
const lines = body.items.map((line) => {
const item = items.find((i) => i.id === line.menuItemId);
return { menuItemId: item.id, name: item.name,
quantity: line.quantity, unitPricePaise: item.pricePaise };
});
const order = {
id: 100 + orders.length, slot: body.slot, status: 'placed',
items: lines, totalPaise: lines.reduce(
(sum, l) => sum + l.quantity * l.unitPricePaise, 0),
};
orders.push(order);
return reply(201, { order });
}
return reply(404, { error: { code: 'not_found',
message: `The mock does not answer ${route}.` } });
}The mock answers six requests, who is signed in, signing out, the menu, the slots, my orders and placing an order: everything the menu page and the orders page need to be drawn and to place an order. It answers with the shapes of JSON the API specification of Chapter 30 promises, and data chosen to show every case a page must handle: an item with plenty left, one with only 4 left, one sold out. mockFetch returns a real Response object, the same kind of thing fetch returns, which is why api.js can use either without knowing which.
Its limits, and why they are acceptable. It never refuses an order and cannot cancel one, so the pages' handling of sold_out and slot_closed, and cancelling, were tried against the real server. It signs nobody in or up: its user, Priya, is always signed in already. And it knows only that one user, a student, so the counter's and the owner's pages were built against the real server once its routes answered. A mock is a tool for starting early, not a substitute for the real thing, and the book's tests all run against the real server (Chapters 50 to 55).
Frontend Implementation, Part 1: the Pages, the Stylesheet and Talking to the Server
Checking that the server hands out the pages
The browser is the real test of a page: open it, look at it at 320 pixels wide in the developer tools' device view, and watch the console for errors. What can be checked from a terminal is that every file is served, with the type the browser needs:
$ cd ~/canteen-preorder
$ npm start > server.log 2>&1 &
$ curl -s -o /dev/null --retry 10 --retry-connrefused localhost:3000/api/health
$ for p in / /menu.html /orders.html /counter.html /owner.html \
> /css/app.css /js/api.js /favicon.svg /nowhere; do \
> curl -s -o /dev/null -w "%{http_code} %{content_type} $p\n" \
> localhost:3000$p; done
200 text/html; charset=utf-8 /
200 text/html; charset=utf-8 /menu.html
200 text/html; charset=utf-8 /orders.html
200 text/html; charset=utf-8 /counter.html
200 text/html; charset=utf-8 /owner.html
200 text/css; charset=utf-8 /css/app.css
200 text/javascript; charset=utf-8 /js/api.js
200 image/svg+xml /favicon.svg
404 text/html; charset=utf-8 /nowhereThe address / is answered with index.html, the sign-in page. Each file comes with its type, text/html, text/css, text/javascript or image/svg+xml, which is what lets the browser treat it as a page, a stylesheet, a script or a picture. And an address that does not exist answers 404 with the HTML page above, not an error in JSON: JSON is for the API's own addresses (Chapter 48).
Do this for your project
- Give every page the same skeleton: the viewport tag, the stylesheet, one module script, a header, one
<h1>, a message area. - Tie every field to a visible label, and give every field the
autocompletevalue that says what it holds. - Repeat the server's rules in
required,minlengthandmaxlengthfor the user's sake, and never rely on them. - Write the stylesheet for the narrowest phone first, in
rem, with colours as named variables, and add wide-screen rules in one media query. - Put every request through one module that speaks JSON and turns every failure, including no connection at all, into one kind of error.
- If the server is not ready, build against a mock that follows the API specification exactly, and say what it does not cover.
Mistakes that cost marks
No viewport tag, and a page that shrinks to a postage stamp on a phone.
Frontend Implementation, Part 1: the Pages, the Stylesheet and Talking to the Server
Placeholders instead of labels: grey text that vanishes as the user types, and that a screen reader may never read.
fetch calls scattered through every page, each handling errors its own way, or not at all.
Colours typed in fifty places, so a change of brand colour is fifty edits and a missed one.
A desktop design squeezed onto a phone instead of a phone design given more room.
The mock left switched on, so the demonstration shows made-up data. Here it takes ?mock=1 in the address, and nothing else turns it on.
Quick revision
- Pages: viewport tag, one module script, labels tied to fields,
autocomplete, arole="alert"message area,aria-current,aria-live. - Browser checks (
required,minlength) are for the user; the server decides. - Stylesheet: phone first, one media query at 48rem,
remunits, colours as variables,:focus-visible, colour never alone. api.js: one door to the server; JSON both ways; the cookie travels by itself; every failure anApiError.- The mock: the same shapes as the API, for starting early; limited to the student's side.
Questions you must be able to answer
1. Why does every page carry <meta name="viewport" content="width=device-width, initial-scale=1">? Because without it a phone lays the page out as if it were a desktop screen and shrinks it to fit, so text is tiny and every tap misses. The tag tells the phone to use its own width, which is what a stylesheet written for phones expects.
2. The browser already checks the form. Why does the server check again? Because the browser's checks can be switched off or bypassed by anyone who sends a request without the page, so they only help honest users catch mistakes early. The server's checks are the ones that protect the data, and it makes them on every request.
3. What does "mobile first" mean in the stylesheet? The rules outside any media query are the layout for a narrow phone, and a media query adds rules only for wider screens. A design made for the phone and given more room works everywhere, while a design made for a desktop and squeezed breaks on phones.
4. Why does every request go through one function, api()? So that JSON is sent and read in one way, every failure, including a failure to reach the server at all, becomes one kind of error with one way of being shown, and a change to how requests work is made in one place.
5. How does the sign-in cookie reach the server if no script handles it? The browser sends a site's cookies with every request to that site by itself, and the pages and the API are served from the same site. The cookie is HttpOnly, so scripts cannot read it at all, which is exactly what protects it from injected scripts.
Frontend Implementation, Part 1: the Pages, the Stylesheet and Talking to the Server
6. What was the mock for, and what could it not do? It let the student's menu and orders pages be built and tried before the server existed, by answering the same requests with the same shapes of JSON the API specification promises. It could not sign anyone in, refuse or cancel an order, or act as the counter or the owner, so those behaviours and pages were tried against the real server.
The rest of this subject
These notes are cut from the University's printed syllabus. Open the syllabus itself for the same subject.