Tips on REST API Error Response Structure

Some suggestions on what information could be included in the error response and why it could be useful

Pagis
Pagis
Sep 1, 2019 · 6 min read
Photo by Mahesh Ranaweera on Unsplash

There are a lot of blogs and discussions around the internet about REST error responses and what could be “The Best response structure”, yet in only in a few posts did I see those magical words:

“It depends on what you’re doing.”

This sentence should be the main answer that should generate some thoughts about design and raise questions for use cases of REST service. You can investigate responses from companies like Facebook, Google, Twitter, etc., but I am 100% sure that you will still create your own response structure, based on your actual needs at that particular time and your architecture. Here are some tips on how to create it efficiently or at least give some thoughts about advanced usage.


Let’s assume that on the following request:

curl -X POST \
https://localhost/rest/api/v1/register \
-d '{
"username": "admin",
"password": "password123",
"confirmPassword": "password123",
"email": "4302aaf2-135b-47c2-b521-8b567ba1b23f@example.com",
}'

The following error response is received:

{
"error": {
"code": "alreadyExists",
"detail": "account already exists"
}
}

This is a simple error response and it’s perfect for handling business case errors. Why? Because business errors tend to be singletons. It allows API clients to relay error codes and handle any logic on the client-side. Implementation of such response structure is also quite simple.

The downside is that the structure is not suitable for request validations since it cannot contain information about multiple errors. We can quickly fix this by adding errors into the array.


After additional changes, let’s assume that on the following request:

curl -X POST \
https://localhost/rest/api/v1/register \
-d '{
"x-username": "user-43a46ff3-784f-440f-b0e5-8291a4a8403c",
"x-password": "password123",
"confirmPassword": "password123",
"email": "4302aaf2-135b-47c2-b521-8b567ba1b23f@example.com",
}'

The following error response is received:

{
"errors": [
{
"code": "missingUsername",
"detail": "username field is missing"
},
{
"code": "missingPassword",
"detail": "password field is missing"
}
]
}

Now we have multiple errors in response, which are suitable in the request validation scenario. For small services this would be a perfect fit. However, if such an approach is used in bigger scale applications then you will likely run into trouble.

Based on the provided example each validation error should now have its own error code. Imagine the situation where you have to check for required, min max, and so on. Each check having its own error code to know what is wrong with the request. Can you imagine what kind of hell that would be?

We can avoid this by including some additional fields in our error structure.


Now for the last time, let’s assume that on the following request:

curl -X POST \
https://localhost/rest/api/v1/register \
-d '{
"username": "a",
"password": "password123",
"confirmPassword": "123456789",
"email": "1b3ee58f-0092-4569-893a-8a648f697b77",
}'

The following error response is received:

{
"errors": [
{
"placement": "field",
"title": "value too short",
"detail": "field username must be at least 4 symbols",
"location": "username",
"field": "username",
"code": "validation.min",
"expression": "min",
"argument": "4",
"traceid": "74681b27-b1ea-454d-9847-d27059e19119",
"stacktraces": [
{
"file": "model/response.go",
"function": "model.(*ErrorMessage).LogStacktraceWithErr",
"linenumber": 22,
"realerror": null
},
{
"file": "helpers/validator.go",
"function": "helpers.Validator",
"linenumber": 58,
"realerror": null
},
{
"file": "handlers/registration.go",
"function": "handlers.RegistrationHandler",
"linenumber": 61,
"realerror": null
}
]
},
{
"placement": "field",
"title": "must be equal",
"detail": "field password is not equal to field confirmpassword",
"location": "password",
"field": "password",
"code": "validation.equal",
"expression": "equal",
"argument": "confirmpassword",
"traceid": "e017ecb2-d72f-4f79-889f-6c42126970a8",
"stacktraces": [
{
"file": "model/response.go",
"function": "model.(*ErrorMessage).LogStacktraceWithErr",
"linenumber": 22,
"realerror": null
},
{
"file": "helpers/validator.go",
"function": "helpers.Validator",
"linenumber": 58,
"realerror": null
},
{
"file": "handlers/registration.go",
"function": "handlers.RegistrationHandler",
"linenumber": 61,
"realerror": null
}
]
},
{
"placement": "field",
"title": "incorrect email format",
"detail": "field email has incorrect value",
"location": "email",
"field": "email",
"code": "validation.email",
"expression": null,
"argument": null,
"traceid": "cecda6b8-7ce8-4054-8c06-9382320afd78",
"stacktraces": [
{
"file": "model/response.go",
"function": "model.(*ErrorMessage).LogStacktraceWithErr",
"linenumber": 22,
"realerror": null
},
{
"file": "helpers/validator.go",
"function": "helpers.Validator",
"linenumber": 58,
"realerror": null
},
{
"file": "handlers/registration.go",
"function": "handlers.RegistrationHandler",
"linenumber": 61,
"realerror": null
}
]
}
]
}

Well yes, it got pretty big! But let’s check what happened there, what information became available and in what way it can help.

Note: I purposely added null to understand this visually, but best practice would be to omit fields if they do not contain any information.

If the API client application tends to display these errors to the end-user it would be wise to have a suggestion for the error placement. A few examples:

Title — short error summary.

Detail — detailed explanation of the error, which in case of validation errors will be dynamically generated. The field could also contain additional information that is available on argument or expression fields.

If the request is long and contains multiple substructures where the same field name occurs several times, then it would be wise to have location of the failed field. However, IMHO, this is only necessary when you have huge form containing lots of fields in one view and you do not want to make several different calls or have small steppers for each category. You know, your typical corporate business application with horrible user experience, 50 fields on display and another 666 that are summoned by filling those 50 fields.

Key of the faulty field. This can be handy since your client can use same field names in the code as in the request and automatically bind response to that particular field.

Here we either see business error code or a validation error code representing that validation group. The API client can use similar validation codes for their front end and thus handle errors more easily.

This field can be a list of various validation rules ( greater, not equals, equals, min, max, required, etc.). This field should have a predefined set of expressions. All undefined expressions should fall under internal errors to safely implement them later. This field can be combined with the error code field, leaving the parsing logic on the API client-side (not advisable).

This field should be expressions argument, that is either number string or other field key.

This field should contain UUID or other type unique identification value to trace the error. API client can request information based on this field’s value and in case of internal error, you can investigate it further.

This is an optional field and could be used in a development environment to see a file, function, line and real error in the call stack. This allows quick debugging when developing API client. This is entirely optional — you could simply log this information, then in case of error go to the logs and investigate it there. Yet, I find this to be a lot faster than viewing logs. Also, this is internal information and should be hidden in case it is used in production mode!


Here we have an error response that could serve minimal needs with an option to be tweaked. I’m not saying it’s a perfect solution that could fit your needs, but it should help you think in the right direction when designing an error response. Additional ideas can be found at RFC7807.


The cherry on the top could be the following response headers:

  • Response-ID— This should be unique UUID per response. It could help the API client to identify that different response was received for the same request with the same content.
  • Correlation-ID— This is more of an internal ID to track down whole flow in the process. But if your service is part of huge ecosystem, then it’s strongly advisable to have this value at all times and pass its value from the beginning to the end of the process.
  • Content-Language —If you’ve implemented multiple languages support you should definitely return a content language to indicate the language used in the title and detail fields.
  • Content-Type: application/problem+json — as suggested in RFC7807 it would be logical to have the following header in case of error.

Better Programming

Advice for programmers.

Pagis

Written by

Pagis

Hate repetitive tasks.

Better Programming

Advice for programmers.

More From Medium

More from Better Programming

More from Better Programming

More from Better Programming

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade