A price changes in DevTools
The interface disables price editing after a product is selected. Someone modifies the outgoing JSON anyway. If the service accepts totalCents: 1, the interface's restrictions have protected nothing.
The browser may offer fast feedback, but the service must decide what it accepts. Our local preview allowed manually entered prices for learning. The remote quote contract now accepts product IDs and quantities instead. This is a deliberate change of authority, not a silent change to the calculator.
Specify the new contract
POST /api/quotes receives { "customerId": "sara", "items": [{ "productId": "bag", "quantity": 1 }] }. The service looks up Sara and the bag in its own fixture data, constructs the Course 02 order shape, and invokes the existing pricing policy. It returns { "quote": { "totalCents": 16200, ... } } with status 200.
Reject unknown customers/products and invalid quantities with 422. Reject malformed JSON with 400. The response is a quote, not confirmation, payment or stock reservation. This lab identifies a fixture customer; it does not authenticate a person. A real service would derive allowed customer access from authenticated identity and permissions, not trust a submitted ID.
Build a pure boundary first
Create server-domain.js outside public/. This mapper accepts trusted catalogue/customer maps and calls your own existing calculator after building the order. Adapt the calculator's import path and export name to your Course 02 project.
export function makePricedOrder(input, customers, products) {
if (!input || typeof input.customerId !== 'string' ||
!Array.isArray(input.items) || !input.items.length || input.items.length > 20) {
throw new Error('Invalid quote request');
}
const customer = customers.get(input.customerId);
if (!customer) throw new Error('Unknown customer');
const seen = new Set();
const items = input.items.map(line => {
if (!line || typeof line.productId !== 'string' ||
!Number.isSafeInteger(line.quantity) || line.quantity < 1 ||
seen.has(line.productId)) throw new Error('Invalid item');
const product = products.get(line.productId);
if (!product) throw new Error('Unknown product');
seen.add(line.productId);
return { name: product.name, unitPriceCents: product.unitPriceCents,
quantity: line.quantity };
});
return { id: 'REMOTE-PREVIEW', customer: { ...customer }, items };
}
This mapper intentionally ignores client prices and totals. The calculator must still validate the customer and guard arithmetic overflow; the mapper is not a replacement for those rules. Define Sara as { name: 'Sara', type: 'premium', active: true } and use the same product fixtures as the GET endpoint.
Connect the HTTP boundary
A request body arrives as a stream of chunks rather than one ready-made object. for await...of waits for successive chunks, extending the asynchronous waiting you studied in Course 02 to iteration. Node represents these binary chunks as Buffers. Count each chunk's length in bytes before keeping it; after the bounded read, Buffer.concat(chunks).toString('utf8') turns the accumulated bytes into text. Only then parse JSON and validate its meaning. This sequence separates transport size, text syntax and business validity.
Add a POST route before the server's method guard. Read the request with for await (const chunk of request), accumulate Buffers and count their bytes; reject above 8192 bytes with 413. Require an application/json content type (allow a charset parameter), otherwise 415. Parse the buffered UTF-8 text in its own try/catch for 400. Then map and calculate in a separate validation block for 422. Unexpected server errors remain 500 in the outer handler. Do not return internal stack traces.
Use fetch('/api/quotes', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(request) }) from your controller, check status and validate the quote fields before displaying them. Keep server-calculated and local-preview results explicitly labelled.
Evidence
Submit the baseline, quantity 2, an unknown product, a duplicate product, a negative quantity and a body containing a fake totalCents: 1. Expect 16200, 32400, rejection, rejection, rejection and still 16200 respectively. Also test malformed JSON and an oversized body at the transport boundary. Explain which layer rejected each request.