Responses
The shape of success responses, pagination metadata and how fields are kept out of the output
Every handler that StarkForge provides answers in the same JSON shape. A client reads success to see the outcome, shows message to a person and takes the result from data, without learning a new format for each route. This page describes that shape, how lists are paginated, which fields never appear in a response and which headers come with it. Failures have a shape of their own, described in Errors.
The Response Envelope
A request for one product shows every part of the shape:
GET /api/v1/products/665f1c2e8b3a4d0012a10001HTTP/1.1 200 OK
Content-Type: application/json
{
"success": true,
"message": "Product fetched successfully.",
"data": {
"name": "Mechanical keyboard",
"price": 89.9,
"sku": "KB-001",
"_id": "665f1c2e8b3a4d0012a10001",
"createdAt": "2026-01-15T10:30:00.000Z",
"updatedAt": "2026-01-15T10:30:00.000Z"
}
}| Key | Present | Meaning |
|---|---|---|
success | Always | true for every success response. Error responses use false |
message | Always | One sentence for people, such as Product fetched successfully. |
data | When the handler has a result | A document, a list of documents, or a small object such as { deleted: 2 } |
meta | Lists only | The pagination numbers described below |
| Other keys | Some sign-in responses | accessToken and refreshToken when you turn them on, mfaRequired and mfaToken when a second factor is needed, resetToken after a password reset code is verified |
A response has no data key when the handler has nothing to return. Signing out is the example, and it also clears both session cookies:
POST /api/v1/auth/logoutHTTP/1.1 200 OK
Content-Type: application/json
{
"success": true,
"message": "Logged out successfully."
}Show message to a person if you like, but make decisions in code from success, the HTTP status and, for failures, the error code. Only error codes are documented as stable.
Status and Message by Handler
A handler always answers with the same status and message pattern. Product in the messages is the model name of the collection, and the numbers are counts:
| Handler | Status | Message | data |
|---|---|---|---|
read | 200 | Product fetched successfully. | The document |
readAll | 200 | Product list fetched successfully. | A list of documents, with meta |
create | 201 | Product created successfully. | The new document |
createBulk | 201 | 2 Product items created successfully. | A list of the new documents |
update | 200 | Product updated successfully. | The document after the change |
remove | 200 | Product deleted successfully. | The deleted document |
removeMultiple, removeAll | 200 | 2 Product items deleted. | { deleted: 2 } |
addFile, updateFile, deleteFile | 200 | File added successfully., File updated successfully., File deleted successfully. | The document with its file list |
The account and sign-in handlers follow the same shape with messages of their own.
Lists and Pagination
readAll returns one page at a time. This request asks for the first page of two products, most expensive first:
GET /api/v1/products?page=1&limit=2&sort=-priceHTTP/1.1 200 OK
Content-Type: application/json
{
"success": true,
"message": "Product list fetched successfully.",
"data": [
{
"name": "Mechanical keyboard",
"price": 89.9,
"sku": "KB-001",
"_id": "665f1c2e8b3a4d0012a10001",
"createdAt": "2026-01-15T10:30:00.000Z",
"updatedAt": "2026-01-15T10:30:00.000Z"
},
{
"name": "Desk lamp",
"price": 34,
"sku": "LP-003",
"_id": "665f1c2e8b3a4d0012a10003",
"createdAt": "2026-01-15T10:30:00.000Z",
"updatedAt": "2026-01-15T10:30:00.000Z"
}
],
"meta": {
"page": 1,
"limit": 2,
"total": 3,
"pages": 2
}
}Three query parameters control the page. All of them are optional:
| Parameter | Default | Rule |
|---|---|---|
page | 1 | A whole number from 1 up, written with digits only |
limit | 50 | A whole number from 1 up. A value above 100 is lowered to 100 without an error |
sort | -createdAt,-_id | A comma-separated list of up to five fields. A leading - sorts in descending order |
The two limits are the options defaultPageSize and maxPageSize, described in Configuration. You can sort by _id, createdAt, updatedAt and every field of the schema, except fields that a client could not read: secret, hashed, encrypted and hidden fields. Read All covers sorting in full.
The meta object describes the whole list, not only the page:
| Key | Meaning |
|---|---|
page | The page that this response holds |
limit | The page size in effect, after the maximum is applied |
total | How many documents match, across all pages |
pages | How many pages there are. It is never less than 1 |
A broken query answers 400 with the code INVALID_QUERY:
| Request | Message |
|---|---|
?page=0 | 'page' must be a positive integer. |
?limit=2e3 | 'limit' must be a positive integer. |
?sort=secretField | Cannot sort by 'secretField'. The hint lists the fields that can be sorted |
A page past the end is not an error. It answers 200 with an empty data list and the same meta, and an empty collection answers with total 0 and pages 1. Compare meta.page with meta.pages to know when to stop.
What a Response Never Contains
StarkForge cleans every document before it writes the response. These fields are removed, whatever the client asks for:
| Removed | Why |
|---|---|
password, tokenHash, codeHash and resetTokenHash | Credentials and token digests never leave the server, and a route cannot opt back in |
Any text that starts with $argon2 | Password hashes are dropped wherever they appear, including inside nested objects |
Fields with select: false | The database does not load them, and a response never includes them |
| Hashed fields | A field that a route lists in hashedFields is hidden for the whole collection |
| Encrypted values | A sealed value is removed unless the route lists the field in decryptedFields. Sealed values inside nested objects, such as a populated document, are always removed |
__v | The Mongoose version key is hidden unless a route opts back in with "!__v" in hiddenFields |
A route can hide more. Add config: { hiddenFields: ["internalNotes"] } to remove a field from the responses of that route only, and put "_id" in the list to drop the id too. A reference field normally holds an id. With config: { populate: "owner" } the response holds the referenced document instead, limited to the fields in populateSelect, which defaults to _id so that personal data does not leak by accident. Relations and Hidden and Encrypted Fields cover both in depth.
An after action can also replace data before the response is sent. Actions explains how.
Headers
Every response carries headers that help with debugging and with clients that retry. This is the start of the response to a list request under the default limits:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
X-Request-Id: ad7a9c50-3964-49ef-a597-cda43ad4317f
RateLimit: limit=100, remaining=99, reset=900
RateLimit-Policy: 100;w=900| Header | When | Meaning |
|---|---|---|
X-Request-Id | Every response | Identifies the request in the logs. StarkForge reuses the id that a client sends when it is 8 to 64 letters, digits, underscores or dashes, and makes a UUID otherwise. Error bodies repeat it as requestId |
RateLimit and RateLimit-Policy | Every response | The allowed requests, the requests left and the seconds until the window resets, in the format of the IETF rate limit draft. The example shows the default of 100 requests per 15 minutes for each IP address |
Retry-After | 429 responses | The seconds to wait before trying again |
Set-Cookie | Session responses | Sets or clears the accessToken and refreshToken cookies |
Cache-Control: no-store | Social sign-in, magic link redemption and the two-factor routes that set up, enable, verify or replace codes | Keeps browsers and proxies from storing a response that carries sign-in material |
StarkForge also sets the standard secure headers of helmet, such as X-Content-Type-Options and Strict-Transport-Security.
Field Formats
An _id is a string of 24 hexadecimal characters. Dates such as createdAt and updatedAt are ISO 8601 text in UTC, like 2026-01-15T10:30:00.000Z. Numbers, booleans and arrays keep their JSON types.
Where to Go Next
Read All
List documents with pages, sorting and ownership rules.
Hidden and Encrypted Fields
Control which fields leave the server and encrypt secrets at rest.
:::