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 · 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.

Placement

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.

Location

Field

Code

Expression

Argument

Traceid

Stacktraces


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.

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