HTTP RESTful Standard Return Object

Standardizing the return format from all endpoints is one of the best things you can do for your clients. This will ensure they always receive a specific object and only need to examine certain values based on the HTTP Status Code. It can significantly simplify the process for systems that call your endpoints, known as consumers, and make it easier for them to understand what they are receiving.

At YTGI, we believe that when you request data, you expect it to be delivered in a specific format. If the data returned is not in the expected format, the system that made the request must handle the unexpected format by implementing appropriate logic. The standard return format ensures the data can be processed correctly and efficiently.

Let’s say that a call to the customer endpoint with a customer number should return a customer object, i.e.:

{
    "FirstName": "Jack",
    "LastName": "Yasgar",
    "CompanyName": "Yasgar Technology Group, Inc."
}

A developer will have code that expects this data and is prepared to push the values into an object in their software, allowing them to use or display it for a specific purpose. Now, suppose no customer is found with the passed customer number. What should the endpoint send back? An empty object may suffice, i.e.:

{
}

Many systems may send back a message like:

HTTP: 404
{
    Message: Customer not found
}

I’m noting the “HTTP: 404” to show what HTTP Response Code is typically sent with a return payload. You won’t see that code in text format if you call the endpoint in a browser. You can see all the HTTP Response codes here if you’re interested.

What if an error could be generated if an invalid customer number format is sent? It could return something similar to the above or something like:

HTTP: 400
{
    Error: Invalid Customer Number,
    Message: The value in the customer number was not recognized
}

What if there is an error in the system retrieving the data:

HTTP: 500
{
    Error: There was an error retrieving the customer,
    Message: System exception; please try again later
}

Returning messages in this manner requires the caller to have extensive logic to determine the HTTP code returned and then deserialize the content sent back into different objects. It then needs to have unique code to choose the way to process the issue.

RFC 7807 and RFC 9457

The IETF standardized a recommended proposed standard for returning error messages to API consumers in July 2012 as RFC 7807 and again in April 2021 as RFC 9457. Please don’t assume that we recommend ignoring industry standards, as that is not our goal. However, there are a few things to consider.

  1. Working with dozens of vendors, I’ve only found that a small fraction adhere to this standard
  2. It still requires the consumer to have custom code for specific errors

Returning messages in this manner requires the caller to use considerable logic to determine the HTTP code returned and then deserialize the content sent back into distinct objects. It then needs to have special code to determine how to process the issue.

At YTGI, we design our microservice endpoints always to return the same object structure. The difference is that the “content” section of the object will contain an array of the objects your code expects. If there is an error, the object returned remains the same; the only difference is that you would need to look in a different section of the object to retrieve any details you may require to display or log for this call.

The base structure of this object is:

{
    Metadata: {},
    Content: [],
    Message: {},
    Links: [],
    Validations: []
}

Metadata

The Metadata object will contain properties such as Page, PageSize, Count, TotalCount, Filter, SearchTerm, etc. This is all the data you need to make decisions when displaying your data to the user, whether a single record or a list of records.

Example JSON:

"Metadata": {
	"Count": 0,
	"Offset": 0,
	"Limit": 0,
	"Page": 0,
	"Total": 0,
	"SearchTerm": "",
	"Filter": "",
	"SortExpression": "",
	"SortOrder": "",
	"SortDescending": true,
	"RefId": 0,
	"RefUniqueId": "",
	"ActiveOnly": false,
	"FromDate": "",
	"ToDate": ""
}

Legend:

CountThe count returned in this dataset
OffsetThe offset from the first record in the returned dataset
LimitThe limit of objects that was set during the call (PageSize)
PageThe page of this call, which is multiplied by the Limit for database calls
TotalThe total amount of records that could be returned with the SearchTerm and Filter if they are provide
SearchTermA search term to limit the request, i.e. “John”
FilterA filter above the SearchTerm, i.e. “Company A”
SortExpressionWhat was requested to sort the return with, i.e. “Id”, or if your API can handle more complex expressions
SortOrderSort order returned as what was sent in the request, or a default
SortDescendingWhether the sort should be descending or not as true or false
RefIdThe Reference Id for the return set, this is typically the parent Id. If this was a result of invoices, then the RefId would be the Customer Id
RefUniqueIdThe Reference Unique Id for the return set, this is typically the parent Unique Id. If this was a result of invoices, then the RefUniqueId would be the Customer UniqueId, typically as a GUID.
ActiveOnlyIf the request was for only active records (true) or all records (false). Many systems do not ever delete records, but mark them as inactive in one way or another for reporting purposes
FromDateReturns any FromDate sent in the request to limit the response by date
ToDateReturns any ToDate sent in the request to limit the response by date

Content

The content object will contain an array of the object types you expect from this call. If the call returns no objects, i.e., not found or just no records that match your filters, then this collection will be empty. If the endpoint is only expecting one record, i.e. Users/{id}, you would still return it as one record in the array. YTGI standardizes on still returning this as a 200 if it is a paged result set that contains no records. It only returns as a 404 if it is a search by Id that is not found.

Example JSON:

"Content": [
    {
        "FirstName": "Jack",
        "LastName": "Yasgar",
        "CompanyName": "Yasgar Technology Group, Inc."
    },
    {
        "FirstName": "Jill",
        "LastName": "Yasgar",
        "CompanyName": "Yasgar Technology Group, Inc."
    }
]

Message

The purpose of the message object is to send back any messages related to the return values. The most exciting aspect of having this section is that you can send messages back even if there is content. Other APIs that only send messages in case of failure do not have this capability, except for using HTTP headers, which are not apparent to someone making the call in a test tool, Swagger, or a browser.

Example JSON:

"Message": {
    "Id": 0,
    "SQLLevel": 0,
    "SQLState": 0,
    "Source": "",
    "Message": "",
    "Description": "",
    "Code": "",
    "AdditionalInfo": "",
    "LogLevel": ""
}

Legend:

IdIf this Message has an Id, such as one stored in a table of standard messages
SQLLevelThe SQL Level return value
SQLStateThe SQL State value returned from the database call
SourceThe source of the message, i.e. function or stored procedure etc.
MessageThe short message returned for the call
DescriptionA description of the issue. In many cases it will be a stack trace
CodeIf you have standard return codes for errors, use that code here
AdditionalInfoAny additional information that you want to provide for the response, does not have to be related to any error
LogLevelThe log level of this response, YTGI uses these: “FATAL”, “ERROR”, “WARNING”, “INFO”

Links

The links section allows you to send back HEATEOS links without butchering every return object type you have to add fields for this purpose.

Example JSON:

"Links": [
    {
        "HRef": "",
        "URI": "",
        "Rel": "",
        "Method": "",
        "Type": ""
    }
]

Legend:

HRefThe link, relative or full URI to the location of related data
URIThe link, full URI to the location of related data if data in HRef is relative
RelThe relationship of the data, i.e. next, previous etc.
MethodThe method needed to access the data, i.e. GET, POST
TypeThe description of the type of data, i.e. “stylesheet”, “application/json” etc.

Validations

One of my pet peeves about APIs, especially POSTs, PUTs and PATCH, is when they return a 400 (Bad Request) without any detail about what is bad about the request. More often than not, a required field is left empty in the payload we’re testing with. It could also be that a field value is out of bounds, whether a code that is not legitimate or a field that is too long, etc. It would be nice to know what fields are causing the issue.

With this section of the standard return object, the user can interrogate the Validations collection to see exactly what the issue is that needs to be resolved.

Example JSON:

"Validations": [
    {
        "Id": "",
        "Code": "",
        "Message": "",
        "MessageType": ""
    }
]

Legend:

IdAny standard Id associated with this validation message
CodeStandard code used for the validation message, i.e. F01, W01 etc.
MessageThe message associated with the validation code, could be dynamic or a lookup from a table
MessageTypeThe message type for this message. Rarely used, but could be used to kick off a particular process or response.

When an object is POSTed to an endpoint, there should be some validations. .NET Core automatically validates that any field that is not marked nullable has a value. This is usually not a friendly way to reject a DTO, because it gives no details on what the problem is. I usually turn off this functionality in my Startup.cs like:

services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
    {
        // Stop APIController from automatically validating the modelstate, including children models
        options.SuppressModelStateInvalidFilter = true;
    });

This options setting will allow a model that doesn’t pass basic .NET required field validation to enter your controller method. At that point, it is up to us to validate the DTO as best we can and provide feedback in the Validation or Message section of the RestReturn object when sending a 400 Bad Request response.

For example, a consumer POSTs a customer object without a FirstName and LastName, which are required fields from a business perspective. We could return the HTTP Status of 400 Bad Request along with this object:

HTTP: 400
{
    Metadata: {},
    Content: [],
    Message: {},
    Links: [],
    Validations: [    {
        "Code": "F001",
        "Message": "FirstName is required",
        "MessageType": ""
    },
    {
        "Code": "F002",
        "Message": "LastName is required",
        "MessageType": ""
    }
    ]}

Summary

Your consumers will appreciate the ease of using your endpoints if you provide them with a way to write boilerplate code to interact with your endpoints. Providing them with a standard object return that is predictable, regardless of what happens, will make it much easier for them to code for every eventuality. It also makes code reviews and contract testing easier because everyone on the team knows what format is expected to be returned, rather than each endpoint being developed as a one-off.

Let’s discuss your project’s Success Right Now!

author avatar
Jack Yasgar
Jack Yasgar’s career as a software engineer and architect spans more than two decades. Specializing in software integration projects.