So far Escena Viva only hands things out: catalog, sessions, HTML, CSS. Everything is a GET. What is missing is what turns a website into a platform: receiving. Letting somebody buy a ticket.
And here the module's asymmetry shows up. Reading req.method, req.url or req.headers is immediate, because Node hands you that data already parsed. The body, however, is not: when your handler starts running, the body may still be travelling over the network. req is a readable stream, and reading it is exactly what you learned in Module 3, with two new twists: the chunks are Buffers and cutting them wrong breaks the characters, and whoever sends them is not on your side, so you have to put a limit on them.
By the end you will have src/server/body.js and Escena Viva's first POST: POST /orders, with validation, a call into the domain and a 201 response with its Location header.
Contents
reqis a stream: the body arrives in chunks- Why concatenating into a string breaks the characters
- The right way:
Buffer.concatandfor await...of - The size limit and the
413 - Timeouts and aborted requests
- Parsing according to the
Content-Type POST /orders: validate and call the domain- Responding
201withLocation, or whichever error fits - Idempotency: why a repeated
POSTduplicates orders
req is a stream: the body arrives in chunks
req is a stream: the body arrives in chunksNode invokes your handler as soon as it has finished reading the headers. The body arrives afterwards, in chunks, through the stream's events:
function handleRequest(req, res) {
const chunks = [];
// chunk is a Buffer. Its size is decided by the network, not by you.
req.on('data', (chunk) => chunks.push(chunk));
req.on('end', () => {
const body = Buffer.concat(chunks).toString('utf8');
console.error(`[body] ${body.length} characters in ${chunks.length} chunks`);
res.end('received\n');
});
// Without this listener, a connection cut mid-transfer takes down the process.
req.on('error', (error) => {
console.error('[body] read failure:', error);
res.statusCode = 400;
res.end();
});
}Three things to internalize:
- You do not control the size of each chunk. It depends on the network MTU, on the client and on the system buffers. A small body can arrive in a single
data; a large one, in hundreds. Never assume a chunk is a meaningful unit. - If you do not read the body, it stays there. A handler that responds without consuming
reqleaves unread data in the socket, and withkeep-alivethat can desynchronize the next request on that connection. If you are going to ignore the body, consume it (req.resume()) or destroy the request. reqemitserror. A connection cut mid-transfer produces anerroron the stream, and like everyEventEmitter, if you do not listen for it you lose the process.
- Why concatenating into a string breaks the characters
This is the bug that has been written most often in Node's history:
// WRONG. It works in your tests and fails in production with accents.
let body = '';
req.on('data', (chunk) => { body += chunk; }); // <- implicit chunk.toString()
req.on('end', () => { JSON.parse(body); });The += converts each Buffer to a string separately, and there lies the trap we studied in lesson 03-06: in UTF-8 a character can take several bytes. ó is two bytes (0xC3 0xB3); an emoji, four. If the boundary between two chunks falls in the middle of that sequence, each half is decoded on its own and neither is a valid character: the result is � (U+FFFD), and that substitution is irreversible.
const full = Buffer.from('{"venue":"Sala Bóveda"}', 'utf8');
// We simulate the network splitting the buffer right between the two bytes of the 'ó'.
const cut = full.indexOf(0xc3) + 1;
const chunkA = full.subarray(0, cut);
const chunkB = full.subarray(cut);
console.log(chunkA.toString('utf8') + chunkB.toString('utf8'));
// {"venue":"Sala B��veda"} <- two replacement characters
console.log(Buffer.concat([chunkA, chunkB]).toString('utf8'));
// {"venue":"Sala Bóveda"} <- correctThe perverse part is when it fails: with small bodies there is almost never more than one chunk, so in development it always works. In production, with larger bodies and a real network, it fails now and then and only with accents, emoji or CJK characters. A JSON.parse that blows up "randomly" and a user named "Bóveda" that sometimes gets saved wrong.
The rule is absolute: accumulate Buffers and decode once, at the end. The alternative, if you need to process text as it arrives, is the StringDecoder from lesson 03-06, which holds on to the incomplete bytes between one chunk and the next.
- The right way:
Buffer.concat and for await...of
Buffer.concat and for await...ofWith async/await, req is walked with for await...of, because readable streams are async iterables (lesson 03-05). It reads far better than loose events:
async function readRawBody(req) {
const chunks = [];
for await (const chunk of req) {
chunks.push(chunk);
}
return Buffer.concat(chunks); // a single Buffer with all the bytes
}Advantages over the on('data') version: stream errors become an exception your try/catch picks up, end is simply the end of the loop, and the result fits the rest of your asynchronous code. This is the form we will use.
But as it stands, that function is an invitation to have your server taken down. What is missing is in the next section.
- The size limit and the
413
413readRawBody accumulates in memory everything that arrives. If a client sends a 5 GB body, your process tries to hold it in RAM and dies out of memory. No bad faith is required: a badly written loop in a client is enough. And with bad faith, it is a denial-of-service attack that costs one line of curl.
A server always limits the body size, and it does so twice:
- By checking
Content-Lengthbefore reading anything. It is cheap and instantly rejects the obviously enormous. - By counting the bytes received as they arrive. Essential, because
Content-Lengthis written by the client: it can lie, or not be there at all if the request usesTransfer-Encoding: chunked.
// src/server/body.js
// Reading a request body with a mandatory size limit.
const BYTE_LIMIT = 64 * 1024; // 64 KB: more than enough for a JSON order
function domainError(code, message) {
const error = new Error(message);
error.appCode = code;
return error;
}
async function readBody(req, byteLimit = BYTE_LIMIT) {
// 1. Cheap filter: what the client SAYS it is going to send.
const declared = Number(req.headers['content-length']);
if (Number.isFinite(declared) && declared > byteLimit) {
throw domainError('BODY_TOO_LARGE',
`The body declares ${declared} bytes and the limit is ${byteLimit}`);
}
// 2. Real filter: count what actually arrives.
const chunks = [];
let received = 0;
for await (const chunk of req) {
received += chunk.length;
if (received > byteLimit) {
// Cut it off: otherwise the client keeps sending what we already discarded.
req.destroy();
throw domainError('BODY_TOO_LARGE', `The body exceeds ${byteLimit} bytes`);
}
chunks.push(chunk);
}
return Buffer.concat(chunks);
}The req.destroy() is not a detail: without it, the client keeps transmitting and you keep receiving bytes you throw away, burning bandwidth and CPU. Destroying the request turns off the tap.
BODY_TOO_LARGE gets added to the http-errors.js table with status 413 Payload Too Large:
About the limit's value: 64 KB is generous for a JSON order and ridiculous for uploading an image. The answer is not "a big limit just in case", but a different limit per route: 64 KB on the API, and several megabytes only where files really do get uploaded.
- Timeouts and aborted requests
There is an attack subtler than the huge body: the excruciatingly slow body. A client announces 60 KB and sends them at one byte per minute. It never exceeds the size limit, but it keeps a connection and a handler busy for hours. With a few hundred connections like that, you exhaust the server. This is the attack known as slowloris.
The defense is a timeout:
// If the request does not complete in 10 s, it gets cut off.
req.setTimeout(10_000, () => {
console.error('[body] request too slow, cutting it off');
req.destroy();
});The symmetrical case is the aborted request: the user closes the tab or loses signal mid-upload. Node signals it on the stream, and it is worth detecting so you do not do useless work:
// The 'aborted' event has been deprecated since Node 16: use 'close'.
req.on('close', () => {
if (!req.readableEnded) console.error('[body] the client aborted the upload');
});The key distinction is req.readableEnded: if it is true, the body arrived complete and the close is the normal one afterwards; if it is false, the connection was cut early. When that happens, do not respond: there is nobody listening, and any heavy work you were about to do (writing to disk, calling another API) is time thrown away.
- Parsing according to the
Content-Type
Content-TypeA body is bytes; what they mean is stated by the Content-Type header. These are the three formats you will see:
Content-Type |
Who sends it | How it is parsed |
|---|---|---|
application/json |
APIs, fetch, mobile apps |
JSON.parse inside a try/catch |
application/x-www-form-urlencoded |
A classic HTML <form> |
new URLSearchParams(text) |
multipart/form-data |
A <form> with <input type="file"> |
With a library, never by hand |
We add to src/server/body.js:
// Returns the body already interpreted according to its Content-Type.
async function readParsedBody(req, byteLimit = BYTE_LIMIT) {
const raw = await readBody(req, byteLimit);
if (raw.length === 0) return {};
// The Content-Type can carry parameters: 'application/json; charset=utf-8'.
const type = (req.headers['content-type'] ?? '').split(';')[0].trim().toLowerCase();
const text = raw.toString('utf8'); // a SINGLE decoding, at the end
if (type === 'application/json') {
try {
return JSON.parse(text);
} catch (error) {
// Useful message: JSON.parse states the exact position of the failure.
throw domainError('INVALID_JSON', `The body is not valid JSON: ${error.message}`);
}
}
if (type === 'application/x-www-form-urlencoded') {
// URLSearchParams decodes percent-encoding and '+' for us.
return Object.fromEntries(new URLSearchParams(text));
}
throw domainError('UNSUPPORTED_TYPE', `Unsupported Content-Type: ${type || '(missing)'}`);
}
module.exports = { readBody, readParsedBody, BYTE_LIMIT };Details that matter. The Content-Type is cut at the ;, because it nearly always comes with parameters. The JSON.parse goes inside a try/catch and the message is put to use: Unexpected token } in JSON at position 42 tells the client exactly where to look, and that is worth far more than an "invalid request". And Object.fromEntries over URLSearchParams loses repeated keys (it keeps the last one): if your form has multiple checkboxes, use getAll explicitly.
About multipart/form-data: it is a format with randomly generated boundaries, parts with their own headers, binary files that must not go through memory and legacy encodings. Parsing it by hand is an error of judgment: it is solved with busboy or multer, which we will see in Module 6. Knowing that you should not do it by hand is part of the craft.
POST /orders: validate and call the domain
POST /orders: validate and call the domainWith the pieces ready, Escena Viva's first POST. The expected body is minimal: {"sessionId": "ses-001-1", "quantity": 2, "email": "[email protected]"}.
Validation is done by hand and before touching the domain. Every check throws with its error.appCode, and the central handler from 04-03 turns them into the right status:
// src/server/order-routes.js
const MAX_TICKETS_PER_ORDER = 6;
function validateOrder(body) {
const { sessionId, quantity, email } = body;
if (typeof sessionId !== 'string' || sessionId.trim() === '') {
throw domainError('INVALID_PARAMETER', 'The "sessionId" field is required');
}
if (!Number.isInteger(quantity) || quantity < 1) {
throw domainError('INVALID_QUANTITY', 'The "quantity" field must be a positive integer');
}
if (quantity > MAX_TICKETS_PER_ORDER) {
// 422: perfectly understood, but forbidden by a business rule.
throw domainError('ORDER_LIMIT_EXCEEDED',
`Maximum ${MAX_TICKETS_PER_ORDER} tickets per order, ${quantity} were requested`);
}
if (typeof email !== 'string' || !email.includes('@')) {
throw domainError('INVALID_PARAMETER', 'The "email" field is not valid');
}
return { sessionId: sessionId.trim(), quantity, email: email.trim().toLowerCase() };
}Four criteria behind these checks:
Number.isInteger, notparseInt. WithparseInt('2 tickets')you would get2and wave through nonsense. If the body is JSON,quantitymust arrive as a number; a"2"string is a badly written client and deserves a400.400is distinguished from422.quantity: 0is a400(the value makes no sense);quantity: 8is a422(the value is valid and the business rule forbids it).- Validation happens before calling the domain. The sooner the invalid is rejected, the less half-done state there is to undo.
- Normalization happens at the end. Trimming spaces and lowercasing the email prevents "[email protected]" and "[email protected]" from being two different customers.
With the data already clean, the handler only orchestrates:
router.post('/orders', async (req, res) => {
const body = await readParsedBody(req); // 413, 400 or 415
const { sessionId, quantity, email } = validateOrder(body); // 400 or 422
const events = await getCatalog();
const event = events.find((candidate) => candidate.findSession(sessionId));
if (!event) {
throw domainError('SESSION_NOT_FOUND', `Session ${sessionId} does not exist`); // 404
}
// The domain decides: throws INSUFFICIENT_CAPACITY (409) if they don't fit.
event.reserve(sessionId, quantity);
const manager = new SalesManager(events);
const tickets = manager.recordSale(sessionId, quantity, `ord-${Date.now()}`);
sendJson(res, 201, { /* ...section 8... */ });
});Notice what is not there: not a single try/catch, not a single mention of an HTTP status number. The handler throws in the domain's vocabulary and the dispatcher translates. That is the payoff of the two previous lessons.
- Responding
201 with Location, or whichever error fits
201 with Location, or whichever error fitsA POST that creates a resource responds 201 Created with the Location header pointing at where it ended up:
const orderId = `ord-${Date.now()}`;
sendJson(res, 201, {
orderId,
sessionId,
quantity,
email,
tickets: tickets.map((ticket) => ticket.code), // EV-2026-000001, ...
amountCents: quantity * session.priceCents,
availableTickets: event.availableTickets(sessionId)
}, { Location: `/orders/${orderId}` });The Location is not decorative: it is what lets a client store the order's URL without building it out of guesswork. And the amount goes in whole cents, the project's convention since Module 1: 2 × 2500 = 5000, never 50.00 in floating point.
Every error path, in one table:
| Situation | error.appCode |
Status |
|---|---|---|
| Body larger than 64 KB | BODY_TOO_LARGE |
413 |
Unsupported Content-Type |
UNSUPPORTED_TYPE |
415 |
| Malformed JSON | INVALID_JSON |
400 |
sessionId missing or the email is not valid |
INVALID_PARAMETER |
400 |
quantity is not a positive integer |
INVALID_QUANTITY |
400 |
| More than 6 tickets | ORDER_LIMIT_EXCEEDED |
422 |
| The session does not exist | SESSION_NOT_FOUND |
404 |
| Not enough tickets left | INSUFFICIENT_CAPACITY |
409 |
And the full check from the terminal:
CT='Content-Type: application/json'
# 201 Created, with Location and the ticket codes
curl -i -X POST http://localhost:3000/orders -H "$CT" \
-d '{"sessionId":"ses-001-1","quantity":2,"email":"[email protected]"}'
curl -s -X POST http://localhost:3000/orders -H "$CT" \
-d '{"sessionId":"ses-001-1","quantity":8,"email":"[email protected]"}' # 422
curl -s -X POST http://localhost:3000/orders -H "$CT" \
-d '{"sessionId":"ses-999-9","quantity":1,"email":"[email protected]"}' # 404
curl -s -X POST http://localhost:3000/orders -H "$CT" \
-d '{"sessionId":"ses-002-1","quantity":5,"email":"[email protected]"}' # 409: 2 left
curl -s -X POST http://localhost:3000/orders -H "$CT" -d '{not json}' # 400Session ses-002-1 has a capacity of 120 and 118 sold: asking for 5 returns 409 with the domain's message, which says how many are left. That detail —an error that explains the real state— is what makes an API usable.
- Idempotency: why a repeated
POST duplicates orders
POST duplicates ordersRun the curl that creates the 2-ticket order twice. You will get two different orders and four tickets sold. That is correct by the spec: POST is not idempotent by definition, and repeating it means creating another resource.
| Method | Idempotent? | Repeating it means |
|---|---|---|
GET, HEAD |
Yes | Reading again; nothing changes |
PUT, DELETE |
Yes | Leaving the resource in the same final state |
POST |
No | Creating another resource |
The problem is that in real life requests get repeated unintentionally: the user clicks "Buy" twice, the network drops after the server processed the order but before the response arrived, or a mobile client retries automatically. On a ticketing platform that means charging twice.
The three defenses, in order of solidity:
- On the client: disable the button after the first click. Necessary, insufficient: it does not protect against a network retry.
- Idempotency key: the client generates a unique identifier per attempt and sends it in an
Idempotency-Keyheader. The server stores it with the response; if the same key arrives again, it returns the stored response without executing anything again. This is what payment gateways do. - Database transactions: reserving capacity and creating the order in one atomic operation, with a uniqueness constraint that prevents the duplicate.
Our server today can do neither 2 nor 3 properly, and it is worth being honest about why: the state lives in memory and in a JSON file on disk, with no transactions and no locks. What is more, two simultaneous requests can read the same available capacity and sell the same two seats, a classic race condition. We will solve it for real in lesson 07-06, with transactions. Until then, keep it in mind as what it is: a known limitation, not an oversight.
Common Mistakes and Tips
- Accumulating the body into a string with
+=. It breaks multibyte characters intermittently.Buffer.concatand decode at the end. - Reading the body with no size limit. A client exhausts your memory with one line of
curl. CheckContent-Lengthand count the bytes. - Trusting
Content-Lengthalone. The client writes it and withchunkedit does not even exist. - Not destroying the request when the limit is exceeded. You keep receiving what you already decided to throw away.
JSON.parsewith notry/catch. A malformed body gives you a500instead of a400, and one stack trace in the log per clumsy client.- Comparing the
Content-Typewith===. It almost always carries; charset=utf-8. Cut it at the;. - Responding
200to a creation, or parsingmultipart/form-databy hand. APOSTthat creates returns201withLocation; for multipart,busboyormulter. - Tip: validate before touching the domain and normalize at the end (trim, lowercase). Rejecting is cheaper than undoing.
- Tip: in error messages, include the failing field and the expected value.
"Maximum 6 tickets per order, 8 were requested"is worth infinitely more than"Invalid request".
Exercises
Exercise 1: demonstrating the multibyte breakage
Write src/lab/break-utf8.js that builds the Buffer for '{"venue":"Sala Bóveda","event":"Concierto de Otoño"}', splits it into 7-byte chunks, and compares two reconstructions: the one from concatenating strings and the one from Buffer.concat. Print both, count the � characters in each and try JSON.parse on both. Repeat with 5-byte and 13-byte chunks.
Exercise 2: the size limit in action
Generate a 100 KB file (node -e "process.stdout.write('x'.repeat(102400))" > /tmp/large.txt) and send it with curl -X POST --data-binary @/tmp/large.txt. Check that the response is 413. Then modify readBody so it logs to stderr how many bytes were actually received before cutting off, and explain why that number almost never matches the limit exactly.
Exercise 3: forms and JSON on the same route
Make POST /orders accept application/x-www-form-urlencoded as well, bearing in mind that in a form every value arrives as a string and quantity has to be converted to a number before validating it. Check with curl -d 'sessionId=ses-003-1&quantity=3&[email protected]' (no -H, because curl already sets that Content-Type) and make sure quantity=three still returns 400.
Solutions
Solution 1. With 7-byte chunks the cut falls inside the ó of "Bóveda" and the ñ of "Otoño":
const original = '{"venue":"Sala Bóveda","event":"Concierto de Otoño"}';
const full = Buffer.from(original, 'utf8');
for (const size of [5, 7, 13]) {
const chunks = [];
for (let i = 0; i < full.length; i += size) chunks.push(full.subarray(i, i + size));
const byString = chunks.reduce((text, chunk) => text + chunk, '');
const byBuffer = Buffer.concat(chunks).toString('utf8');
const broken = [...byString].filter((character) => character === '�').length;
console.log(`${size} bytes -> ${broken} broken characters | correct: ${byBuffer === original}`);
}Buffer.concat returns the original in all three cases; string concatenation breaks characters whenever a cut falls inside a multibyte sequence, and its JSON.parse fails because � is not a valid character inside a JSON string... unless it falls inside a value, in which case it parses with no error and stores corrupt data, which is the worst scenario of all.
Solution 2. The number almost never matches the limit because the check happens after a whole chunk has been received: if the limit is 65,536 bytes and the chunk that crosses it carries 16 KB, you will have overshot by several thousand. That is the correct behavior —you cannot reject half a chunk—, and that is why the limit is chosen with headroom and not to the byte.
if (received > byteLimit) {
console.error(`[body] cut off after ${received} bytes (limit ${byteLimit})`);
req.destroy();
throw domainError('BODY_TOO_LARGE', `The body exceeds ${byteLimit} bytes`);
}Solution 3. The conversion has to be strict: Number('three') is NaN, and Number.isInteger(NaN) is false, so the existing validation already rejects it untouched.
function normalizeNumbers(body, isForm) {
if (!isForm) return body;
// In a form everything arrives as a string: convert what should be a number.
return { ...body, quantity: body.quantity === undefined ? undefined : Number(body.quantity) };
}Using Number and not parseInt is deliberate: parseInt('3 tickets') returns 3 and would accept garbage, while Number('3 tickets') returns NaN and rejects it. Session ses-003-1 has enough capacity, so the 3-ticket order returns 201.
Conclusion
Escena Viva now sells tickets. And the journey has made it clear that receiving is considerably more delicate than handing out. req is a readable stream whose chunks are Buffers of unpredictable size, so the first rule is to accumulate Buffers and decode once with Buffer.concat: concatenating into a string splits multibyte characters and produces unrecoverable � intermittently, exactly the kind of failure that never shows up in development. With for await...of the code also comes out readable and with errors integrated into try/catch.
The second rule is that whoever sends the body is not on your side: src/server/body.js checks Content-Length as a cheap filter and counts the bytes received as the real one, destroys the request when it overshoots and throws BODY_TOO_LARGE → 413. Add to that req.setTimeout against the excruciatingly slow body and abort detection with close plus readableEnded, so you never work for nobody. Parsing is decided by the Content-Type cut at the ;: JSON.parse inside try/catch with its message put to use, URLSearchParams for forms, and multipart/form-data delegated to a library because doing it by hand is an error of judgment.
And you have the first real POST: hand-written validation before touching the domain —Number.isInteger instead of parseInt, 400 for what makes no sense and 422 for what the business rule forbids, with the 6-ticket limit—, a call to reserve and to SalesManager, and a 201 with Location response and an amount in cents. All of it without a single try/catch in the handler: it throws in the domain's vocabulary and the 04-03 dispatcher translates. You also know what is still unsolved: a repeated POST duplicates the order, and two simultaneous ones can sell the same seat. The solution has a name —idempotency keys and transactions— and a date: lesson 07-06.
What is left is to flip the page over. So far Node has always been the server; in the next lesson, Consuming External APIs from Node.js, it will be the client: global fetch and http.request underneath, the classic mistake that fetch does not reject on a 404 or a 500, timeouts with AbortController, retries with exponential backoff reusing retry from Module 2, and a real Escena Viva service —src/services/currency-exchange.js— with an in-memory cache and graceful degradation when the external provider fails.
Node.js Course: From Beginner to Advanced
Module 1: Introduction to Node.js
- What Is Node.js?
- Installing and Setting Up the Environment
- Your First Node.js Program
- The Node.js REPL
- Modern JavaScript for Node.js
- The Course Project: the Escena Viva Platform
Module 2: Core Concepts
- Node.js Architecture
- The Event Loop
- Callbacks and Asynchronous Programming
- Promises and async/await
- Events and EventEmitter
- CommonJS Modules and require()
- ES Modules and Interoperability
Module 3: File System and I/O
- Reading and Writing Files
- The fs Module in Depth
- Cross-Platform Paths with the path Module
- Working with Streams
- Transform Streams and pipeline
- Buffers and Binary Data
Module 4: HTTP and Web Servers
- Creating a Simple HTTP Server
- Handling Requests and Responses
- Manual Routing
- Serving Static Files
- Receiving Data: Request Bodies and JSON
- Consuming External APIs from Node.js
Module 5: NPM and Package Management
- Introduction to NPM and package.json
- Installing and Using Packages
- Semantic Versioning and package-lock
- npm Scripts and Project Automation
- Creating and Publishing Packages
- Dependency Security and Maintenance
Module 6: The Express.js Framework
- Introduction to Express.js
- Setting Up an Express Application
- Routing in Express
- Middleware
- Essential Third-Party Middleware
- Input Data Validation
- Error Handling
Module 7: Databases and ORMs
- Introduction to Databases
- Using MongoDB with Mongoose
- CRUD Operations
- Relationships, Population and Advanced Queries
- Using SQL Databases with Sequelize
- Migrations, Transactions and Seed Data
Module 8: Authentication and Authorization
- Introduction to Authentication
- User Registration and Password Hashing
- Sessions and Cookies with Passport.js
- Authentication with JWT
- Role-Based Access Control
- API Security Best Practices
Module 9: Testing and Debugging
- Introduction to Testing
- Unit Testing with Mocha and Chai
- Test Doubles with Sinon
- Integration Testing
- Coverage and Test Automation
- Debugging Node.js Applications
Module 10: Advanced Topics
- The Cluster Module
- Worker Threads
- Caching and Job Queues with Redis
- Performance Optimization
- Building RESTful APIs
- GraphQL with Node.js
Module 11: Deployment and DevOps
- Configuration and Environment Variables
- Logging and Monitoring in Production
- Using PM2 for Process Management
- Packaging with Docker
- Deploying to Heroku and Other PaaS
- Continuous Integration and Deployment
