By Kenneth Bernholm
Representational State Transfer (REST) Application Programming Interfaces (APIs) seems simple in theory but the implementation process is full of pitfalls. This page explains how to avoid most of them.
The Hypertext Transfer Protocol (HTTP) has nine verbs (CONNECT
, GET
, HEAD
, DELETE
, OPTIONS
, POST
, PUT
, PATCH
, and TRACE
) and all REST paths reponds to more than one of them. The HTTP verbs and the REST paths employs many-to-many relationships, so a resource is not necessarily covered by a simple CRUD, CRAP, or DAVE-like controller. REST is path centric. Not resource centric.
All HTTP verbs are optional but all paths should accept the OPTIONS
verb that reports what other HTTP verbs the path reponds to. Support for the CONNECT
verb is only relevant on the root path (/
) because it's for establishing connections to the server. The TRACE
verb is a loop-back interface for diagnostic purposes which can be a security risk because it can reveal information about the networks travelled. The HEAD
verb returns only the headers of a request.
You can arrange your action methods for each path as you please but do put in an effort to make your controllers discoverable for the next programmer. A workable strategy is to have one controller per path named after the path it covers with one action method per HTTP verb named after the HTTP verb it responds to..
The home controller manages the root path (/
), and a GET
request returns a list of available resources. Let's say the API offers two resources: Offices and Printers. All printers are in an office but not all offices have a printer and some offices have more than one printer. In a complete implementation, the root path should allow the OPTIONS
, CONNECT
, GET
, HEAD
, and TRACE
verbs.
# path: / class HomeController { public function options() { returns a list of allowed verbs (OPTIONS, CONNECT, GET, HEAD, TRACE) } public function connect() {} public function get() { returns a list of available resources (offices and printers) } public function head() {} public function trace() {} }
A sample GET
response:
[ { "name": "Offices", "path": "/offices/" }, { "name": "Printers", "path": "/printers/" } ]
The Offices (plural) controller covers the /offices/
path and its action methods handles access to all the offices as a group and the creation of new offices using the GET
, HEAD
, OPTIONS
, POST
, and TRACE
verbs.
# path: /offices/ class OfficesController { public function options() { returns a list of the allowed verbs (OPTIONS, GET, HEAD, POST, TRACE) } public function get() { returns a list of available resources (individual offices) } public function head() {} public function post() { creates new office from the data in the request body } public function trace() {} }
A sample GET
response:
[ "/offices/{office_id}", ... ]
The Office (singular) controller covers the /offices/{office_id}
path and its action methods handles access to individual offices using the GET
, HEAD
, DELETE
, OPTIONS
, PUT
, PATCH
, and TRACE
verbs.
# path: /offices/{office_id} class OfficeController { public function options() { returns a list of allowed verbs (OPTIONS, GET, HEAD, DELETE, PUT, PATCH, TRACE) } public function get() { returns the office specified by {office_id} } public function head() {} public function delete() { deletes the office specified by {office_id} } public function patch() {i updates the office specified by {office_id} from the data in the request body } public function put() { replaces the office specified by {office_id} from the data in the request body } public function trace() {} }
A sample GET
response:
{ "id": {office_id}, "printers" [ "/printers/{printer_id}", ... ] }
The office printers are listed as paths and not just printer IDs because resources are identified by paths supplied by the REST API. This ensures that the paths can be changed without breaking the interface at the API consumers end.
To access a printer, you leave the office path and switch to the printer path and the printer controllers which should be identical in structure to the office paths and controllers. The reason for this switch is that REST paths are ID centric. Never have more than one ID in a REST path. A path like /offices/{office_id}/printers/{printer_id}
may seem like a perfect representation of real life, because printers are in offices and not the other way around, but it contains redundant information and you should almost always avoid data redundancy.
The database behind the REST API is the source of the office/printer relations. These relations are not decided by the path. If the path contains an {office_id}
and a {printer_id}
that doesn't match up —in the database, the printer specified by {printer_id}
simply isn't in the office specified by {office_id}
— the server doesn't which {id}
is wrong. You would probably receive a response in the lines of ERROR: Identifier conflict
.