API REST - How to write good APIs
When developing APIs from scratch, you can do it in the mode I get behind the PC and start programming or I learn about the standards first and then program in compliance with these standards.
And if we lose sight of these norms, we quickly come back to writing endpoints like /articles/insert
that are semantically incorrect (and even stupid). In fact, we'll never GET
here.
The purpose of the article below is to list best practices by way of examples.
Main conceptsโ
Resourcesโ
Don't use verbs in URLsโ
HTTP has verbs like GET
, POST
, DELETE
, ... so don't use calls like:
/articles/gettitle
,/articles/insert
,/articles/1/delete
But use the adequate HTTP verb:
GET /articles/title
,PUT /articles
,DELETE /articles/1
Prefer plural nounโ
Always use the plural like GET /employees/1
to return the first employee and not GET /employee/1
.
Using the plural make it easier to understand that employees
is a collection and we can use verbs like GET
to get all, one or a range, POST
to add a new employee, PUT
to update a employee's information.
Send the HTTP Accept headerโ
It's really recommended to inform the server about what you expect for: json
, csv
, plain-text
, xml
, ... To do this, use the Accept
header like in the example here above. This is safer because the server can change his default format from JSON to CSV f.i. and if you expect JSON, your code will be broken.
Don't do this:
const employee = axios.create({
baseURL: 'http://localhost:3000/employees/123'
})
But well:
const employee = axios.create({
baseURL: 'http://localhost:3000/employees/123',
headers: {
'Accept': 'application/json'
}
})
Now, in the second example, you tell the web server that you want a JSON representation and nothing else.
Responsesโ
200 OKโ
A server MUST respond to a successful request to fetch an individual resource or resource collection with a 200 OK
response.
When DELETE
was used, we can also return 204 No Content
to inform the calling application that deletion was successful.
201 Createdโ
If the requested resource has been created successfully and the server changes the resource in any way (for example, by assigning an id), the server MUST return a 201 Created
response and the new added resource in body.
202 Acceptedโ
If a request to create/update or delete a resource has been accepted for processing, but the processing has not been completed by the time the server responds, the server MUST return a 202 Accepted
status code.
204 No Contentโ
If the requested resource has been created successfully and the server does not change the resource in any way (for example, by assigning an id or createdAt
attribute), the server MUST return either a 201 Created
status code and the resource in response or a 204 No Content
status code with no response document.
If the deletion was done successfully, 204 No Content
can be used too.
For an update, it's the same: we can send code 200 OK
with the updated resource in the body or, code 204 No Content
without body; just headers.
400 Not Foundโ
You should return a 400 Bad Request
response when the call was incorrect like an unknown value is used for a parameter (like &lang=it
when Italian isn't supported).
403 Forbiddenโ
A server MUST return 403 Forbidden
in response to an unsupported request to, for instance, create a resource (with POST
verb with a client-generated ID
since the ID
has to be generated by the server, not the client).
404 Not Foundโ
A server MUST respond with 404 Not Found
when processing a request to fetch a single resource that does not exist, except when the request warrants a 200 OK response with null as the primary data (as described above).
409 Conflictโ
A server MUST return 409 Conflict
when processing a POST
request to create a resource with a client-generated ID
that already exists.
A server MUST return 409 Conflict
when processing a POST
request in which the resource object's type is not among the type(s) that constitute the collection represented by the endpoint.
Verbsโ
HEADโ
HEAD
has to be used to retrieve information's about the resource like f.i. the number of records in the resource (number of books, articles, ...) or to check if a resource exists.
For instance, imagine a resource called countrylanguage
, we can verify if there are records for countrycode=BEL
:
โฏ curl --head http://localhost:3000/countrylanguage\?countrycode\=eq.BEL
HTTP/1.1 200 OK
Date: Sat, 28 Jan 2023 20:17:08 GMT
Server: postgrest/10.1.1
Content-Range: 0-5/*
Content-Location: /countrylanguage?countrycode=eq.BEL
Content-Type: application/json; charset=utf-8
If we get a HTTP return code 200
, we can run a GET
action to get records:
โฏ curl http://localhost:3000/countrylanguage\?countrycode\=eq.BEL | jq
HEAD
can thus be used to get meta data about a resource.
Count number of items in a resourceโ
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range
HEAD
can be used against a resource to get the total number of records; f.i. to be able to use a navigation bar.
So, don't create an endpoint like http://localhost:3000/countrylanguage/count
when the standard way is using HEAD
.
Imagine a database table called countrylanguage
having 984
records.
This info can be retrieved like this:
โฏ curl --head http://localhost:3000/countrylanguage
HTTP/1.1 200 OK
Date: Sat, 28 Jan 2023 19:14:45 GMT
Server: postgrest/10.1.1
Content-Range: 0-983/*
Content-Location: /countrylanguage
Content-Type: application/json; charset=utf-8
curl --head
(similar to curl -i
) is using the HEAD
HTTP verb.
In order to extract this information in JavaScript, the parse-content-range-header script can perhaps be used.
GETโ
GET
as READ
in CRUD
Return a resource (can be a collection or just one).
Return all employees:
curl -X GET http://localhost:3000/employees
-H "Accept: application/json"
Return the employee #1:
curl -X GET http://localhost:3000/employees/1
-H "Accept: application/json"
POSTโ
POST
as CREATE
in CRUD
Using POST
you'll create a new resource. Calling POST
five times for the same "new" employee will thus create five new employees. POST
is not idempotent meaning calling it more than once will return every time a new resource (like the new employee id)
curl -X POST http://localhost:3000/employees
-d '{"firstname":"christophe",...}'
The example here above will create a new employee, the server will return the HTTP status code 201
(CREATED) and, probably, also return a location-header with a link, like http://localhost:3000/employees/999
i.e. the link to the newly created employee.
PUTโ
PUT
as UPDATE
in CRUD
, when you'll update every fieldUsing PUT
implies you're intended to update each field of the record. If you plan to update just a few of them, take a look to the PATCH
verb.
Using PUT
you'll update an existing resource.
curl -X PUT http://localhost:3000/articles/15
-d '{"title":"Introduction to API", "author":"John"}'
Consider using PUT
only when you'll update every information's of the resource. If you wish to update just a few ones (partial content), Consider using PATCH
.
PUT
is idempotent since running the same update won't have other effects on the server. You can update once, five or one thousand times the title of the article #15, the result will always be the same.
If successfully updated will return the HTTP status code 200
(OK) or 204
(No Content) if nothing is updated. If successfully created will return the HTTP status code 201
(CREATED) (like when using the POST
verb).
Note: depending on the API developer, updating an in-existing resource can return an error ('Resource #15 do not exist') or the developer can decide to create it (and thus do the same thing as the PUT
verb). This is indeed possible since PUT
is used for full content: you've sent to the server every possible field so it's possible to create it.
PATCHโ
PATCH
as UPDATE
in CRUD
, when you'll DON'T update every fieldsIf you plan to update all fields, you need to use PUT
, not PATCH
.
PATCH
is to be used when the update is partial like updating, just, one column: curl -X PATCH http://localhost:3000/employee/1 -d '{"firstname": "Christophe"}'
will, only, update first name.
If successfully updated will return the HTTP status code 200
(OK) or 204
(No Content) if nothing is updated. If successfully created will return the HTTP status code 201
(CREATED) (like when using the POST
verb).
PATCH
on an in-existing resource will return an error while, perhaps, PUT
will create the resource. If's then safer to use PATCH
and not PUT
when updating partial content.
It's indeed impossible to create a new record with PATCH
since the request just mention a few information's, not all.
DELETEโ
DELETE
as ... DELETE
in CRUD
Remove an in-existing resource
To remove employee #59:
curl -X DELETE http://localhost:3000/employees/59
Returned structureโ
Top-Levelโ
A document MUST contain at least one of the following top-level members:
data
: the document's primary data.errors
: an array of error objects.meta
: a meta object that contains non-standard meta-information.
data
and errors
If the query has generated an error, it is not expected to return any data. And vice versa.
The document's primary data is a representation of the resource or collection of resources targeted by a request.
data
can be an array or not:
{
"data": {
[
"key1": "value1",
"key2": "value2",
// ...
],
[
// ...
]
}
}
or, if just one record, directly:
{
"data": {
"key1": "value1",
"key2": "value2",
// ...
}
}
In fact, most of the time, an array is returned, as this allows the API to be modified in the future to return other results without generating BC (break changes) in the API.
Dataโ
As described in Top-Level items, the returned information should be put as a top-level node called data
:
Best is to always use the array notation so we can return one or more rows.
'data': [
[...]
]
Returning an errorโ
As described in Top-Level items, errors should be put as a top-level node and use the plural form:
"errors": [
[...]
]
Prefer to use HTTP status code 400 Bad Request
when the call was incorrect like an unknown value is used for a parameter (like &lang=it
when Italian isn't supported). Use code 500 Internal Server Error
when the server has returned an error.
errors
has to be an array so we can return more than once and should contain at least one of these keys (not exhaustive):
status
: the HTTP status code applicable to this problem, expressed as a string value. This SHOULD be provided.code
: an application-specific error code, expressed as a string value.title
: a short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of localization.parameter
: a string indicating which URI query parameter caused the error.
"errors": [
[
"status": 400,
"code": "ERR_INVALID_VIEW",
"title": "Language code 'it' is not supported",
"parameter": "lang"
],
[...]
]