YASE - Yet Another Software Engineer
After designing the APIs on paper, choosing endpoint names, and deciding on handler functions to manage requests, we can move on to the more concrete part: writing the interfaces. this means defining the payloads—the json objects to be passed to the APIs and returned to the client.
These interfaces usually correspond to a database entity, as we’ve seen before. in fact, APIs often serve precisely to interface with the backend database. so, it frequently happens that these interfaces are nothing more than an object with the same attributes as the entity itself. however, in addition to the fields and data present in the db, it’s good practice to add some metadata to the api interfaces. this metadata can be useful for both the end-user (who might also be a developer) and the API programmer.
So, what’s typically done is to encapsulate the returned data in a standard data structure for that request (or for the entire system). this structure contains some additional information, such as any server-side errors that generated corrupted data or no data at all, debug messages, request timestamps, and so on.
Generally, a structured language like JSON is used for these interfaces, along with best practices for its generation, including mandatory fields that facilitate internal use and client integration. Let’s look at some of the main best practices recommended by google: Google best practices for rest apis.
camelcase
: always use lowercamelcase
format for property names (e.g., username
, creationdate
, listitems
).string
, number
, boolean
, object
, array
, null
)."2025-03-28t08:21:58z"
or "2025-03-28"
). use utc (z
) whenever possible to avoid timezone ambiguities.number
(preferably in seconds or milliseconds, specifying the unit in the property name, e.g., durationseconds
) or as an iso 8601 duration format string (e.g., "p3dt6h4m"
).string
. choose meaningful string values (e.g., "pending"
, "completed"
).null
to explicitly indicate the absence of a value. avoid using empty strings (""
) for this purpose, unless an empty string is a valid value distinct from null
. consider completely omitting optional properties if they don’t have a value.string
.{}
) or, less commonly if appropriate, an array ([]
) as the root structure. it’s often useful to use a root object even for collections (e.g., { "items": [...] }
) to easily add metadata (like pagination information: totalitems
, nextpagetoken
, etc.).
Which fields should you include at a minimum:
1. for general context and success:
data
(or payload
, result
, items
, etc.): a container field for the “actual” requested data. using a wrapper like this allows you to add other fields at the same level without mixing them with the main data.
JSON
{
"data": { "userid": 123, "name": "john doe" }
}
message
/ msg
: a human-readable string providing a brief message about the operation’s status (e.g., “user created successfully,” “data updated”). useful for both successes and errors, and for display on a potential frontend.timestamp
: the date and time (preferably in iso 8601 utc format) when the response was generated by the server. useful for logging and caching.requestid
: a unique identifier for the specific request/response. very useful for tracking and debugging across client and server logs.apiversion
: the version of the API that handled the request.executiontimems
: the time taken by the server to process the request (useful for performance monitoring).2. for detailed error handling (typical with 4xx/5xx status codes):
error
: an object container for all error details.errorcode
/ code
: an application-specific code (string or number, e.g., "validation_error"
, "auth_failed"
, 10021
) that uniquely identifies the error programmatically. it’s distinct from the http status code but provides more detail.errormessage
/ message
/ detail
: a human-readable description of the specific error that occurred.errors
/ details
: an array of objects, useful when there are multiple errors (e.g., validation errors on multiple input fields). each object can contain details like field
(the field with the error), code
(field-specific code), message
(field-specific message).
JSON
{
"error": {
"code": "validation_error",
"message": "invalid input.",
"details": [
{ "field": "email", "code": "invalid_format", "message": "invalid email format." },
{ "field": "age", "code": "value_too_low", "message": "age must be greater than 18." }
]
}
}
You can consider extending the fields of the “error” container object by following the rfc 7807 standard “problem details for http apis” (rfc 7807), which defines fields like type
, title
, status
, detail
, instance
. here’s an example from the standard:
{
"type": "https://example.com/probs/insufficient-funds",
"title": "insufficient funds",
"status": 403,
"detail": "your account balance is 30, but the transaction requires 50.",
"instance": "/account/12345/transactions/67890"
}
To integrate it, you could do something like this:
{
"error": {
"code": "validation_error", // same as status
"title": "insufficient funds", // short title
"message": "invalid input.", // long description, substitutes detail
"details": [
{ "field": "email", "code": "invalid_format", "message": "invalid email format." },
{ "field": "age", "code": "value_too_low", "message": "age must be greater than 18." }
],
"helpurl": "https://example.com/probs/insufficient-funds", // replaces "type"
"instance": "/account/12345/transactions/67890" // points to the resource if it exists
}
}
helpurl
/ documentationurl
: a link to documentation explaining the error and how to resolve it.3. for pagination (when returning collections/lists):
page
/ pagenumber
: the current page number being returned.pagesize
/ limit
: the number of items per page.totalitems
/ totalcount
: the total number of items available in the entire collection (across all pages).totalpages
: the total number of pages available.nextpagetoken
/ continuationtoken
: an opaque token for cursor-based pagination (alternative/complement to pagenumber
)._links
(hateoas style): an object containing hypermedia links, including those for the next (next
), previous (prev
), first (first
), and last (last
) pages.
Important considerations:
4xx
status (e.g., 400 bad request
, 404 not found
), and a server error should return a 5xx
status (500 internal server error
). don’t return 200 ok
with an error message in the body.In summary, including a wrapper with metadata like data
, error
(with code
, message
, details
), requestid
, and pagination fields makes rest APIs more robust, user-friendly, and maintainable.
Let’s put it all together:
# example of a complete response
{
"data": {
"userid": 123,
"name": "john doe"
},
"message": "user found",
"timestamp": "2020-07-10 15:00:00.000",
"requestid": "rdxkm65",
"apiversion": "v1",
"executiontimems": 300,
"page": 1,
"limit": 1,
"totalitems": 1
}
In case of an error:
{
"timestamp": "2021-07-10 15:00:00.000",
"requestid": "rdxkd345",
"apiversion": "v1",
"executiontimems": 121,
"error": {
"code": "validation_error", // same as status
"title": "insufficient funds", // short title
"message": "invalid input.", // long description, substitutes detail
"details": [
{ "field": "email", "code": "invalid_format", "message": "invalid email format." },
{ "field": "age", "code": "value_too_low", "message": "age must be greater than 18." }
],
"helpurl": "https://example.com/probs/insufficient-funds", // replaces "type"
"instance": "/account/12345/transactions/67890" // points to the resource if it exists
}
}
If the data
field is present, then the http return code will be a success code, and the error
container will not be present; otherwise, the error
container will be present, and not the data
one.
It’s advisable to include only one of the two fields, data
or error
, based on the http response code. if the http code indicates success (e.g., 200 ok
), use the data
field. if, however, the http code indicates an error (e.g., 4xx
or 5xx
), use the error
field. this approach keeps the payload structure clear and consistent.