REST-ish

#programming

I've spent a lot of time writing "REST" apis.

I put it like that because I've never really quite gotten an api to follow REST. There's always a little something that doesn't quite fit (or a whole boatload of somethings). So I figured I'd make my own version of it. I call it "REST-ish".

What I Disagree With

A common misconception of REST is that HTTP verbs are part of it. This isn't true. What's required is that resources have a "uniform interface". Technically from a VERB perspective as long as you are consistent you are good. The reason I have that phrase in quotes however is that it's not that simple.

Uniform interface is further defined with four constraints. Two of them I'm 100% on board with. The first is that resources should be identifiable via URIs, and the transfer format has little to do with the storage format. The second is that messages should contain enough information for the client to parse the message (include content-types folks).

The other two I have problems with. The easiest is that hypermedia isn't, and has never been useful. You should never integrate with a system for which you do not already understand how to interface with it, and there is no world where a root resource exists. The idea that you could hit a single endpoint and have a machine understand how to interface with the entire system is entirely fantasy. Hypermedia also just makes it way more difficult to represent object state since you need to clutter responses with information that isn't useful.

The harder to explain constraint is that with only a representation of a resource, you should have enough information to modify that resource. This sounds like it should make sense. I believe it's also the reason why folks confuse HTTP VERBs with being REST. The consequence of that statement is that partial or union views cannot exist. If you only return part of an object, you cannot then have the client manipulate the entirety of it. If you return a "virtual" resource that is a union of other resources, you likely cannot modify that resource at all.

As far as VERBs go, I've found that there's a ton of overlap and confusion on "what" VERB to use. Between PUT and POST to create resources, or who actually implements PATCH. One of my goals is to clarify when to each each VERB as a much more

My Proposal

As this is a web proposal, it'll operate through the terms of HTTP VERBs. If you aren't familiar with what a VERB is, here is a basic explanation. HTTP requests are formed from something like four parts: the VERB, the path, the headers, and the body. VERBs tell the server what action to perform with the path. An example: a GET request to /movies would get you a list of movies (assuming the server has such a function). Think of VERBs as simple (highly standardized) general commands.

I'm not going to add any VERBs for obvious reasons, but I'm going to rework a few. Before that we need something much more foundational regarding paths. I do not believe paths should have more than three segments, with the first segment defining the resource that's being worked on. The reason will be expanded on near the end.

GET

There are three forms of GET. The first, mirroring the standard usage is the root resource. Using the movies example again it would be /movies. This combination would return a listing of movie entries. The resource name is always a plural, and the endpoint can be customized with query parameters. An example being something like /movies?filter=citizen. Paging parameters should also be entered via query string parameters. Adding again to the example /movies?filter=citizen&page=3. This endpoint should return a 200 with both a response containing data and a 200 with an empty array in your transport format of choice if there is no data.

The second, also commonly used this way would be retrieving a single instance of the resource. An example: /movies/:id. Id being the unique identifier for that resource. The format doesn't particularly matter but if you've got a choice, prefer UUIDs. There should be no query parameters when retrieving a single resource this way. The request should respond with a 200 if there is a response and a 404 if there is not.

The final form is where I depart from open standards. This adds a final third segment that represents a view. This would be used in the case you want data related to the resource, from the point of view of that resource. An example is if there are fields that you traditionally wouldn't want to expose, you can use a view to enhance the data from the standard view. You can also use views for things like joining to make larger datasets, but that should follow a set of constrains.

  1. The data is actually combined into a view, and not just a list GET on another resource masquerading as a view.
  2. The view isn't just a different form of search with the normal form of the resource.
  3. If the view is a join with one or more other resources, it should be from the perspective of the resource represented at the root.

Because views can be either single resource results or lists, the status codes would match the appropriate previous example (such as returning a 200 with an empty array if there is no data).

DELETE

Very simple. One way of using it, the same as getting a single resource. Example /movies/:id. The response should return with a 404 of the resource did not exist, and a 204 if it did and was deleted.

PUT

All resource creation and updating should be done through PUT. Updating and creating of a resource should be done on the same route, as an upsert style operation. If we want to create or update a movie we use a PUT to /movies.

You may be asking about how to handle primary keys. They are the reason I am recommending this style of creation and updating. Generally speaking data models will have the primary key as part of the model. If your route gets the id from the path, it makes transport models harder to rationalize as you can't do a simple map over one object to the next as you need the extra information. By making it part of the transport model you remove that problem. Depending on the complexity of your data, you may also be able to remove the transport model entirely.

For server generated primary keys, simply use a nullable or option type as the primary key when doing the initial PUT. The response should contain the entire object, including any generated primary key. Even if you don't generate the primary key, you should still return the entire object.

PATCH

Don't use it. It's a pain to implement, and if you've got the resources to do so you should be using RPCs anyway.

POST

POST should be reserved for actions. As in you need to perform something that isn't quite resource state management. Think of this like the RPC version of regular HTTP VERBs. Because we don't use PUT or POST with a route parameter, there's no problem with route collisions. Similar to PUT, if you need to reference a single item put the reference in the body of the request and not as a route parameter.

There is some nuance here. An example is state manipulation for specific fields that require additional authorization. While I would recommend not using such a pattern, this is where you could do something like that. Personally I would make SSNs a resource with their own updates and explore them with a view like this.

PUT /ssn
GET /client/:id/with_ssn

Should You Use This?

Maybe? I will be. I've been making web services for over a decade and this small list of constraints is something I made to clean up absurdly badly designed APIs. I'll be putting some examples together in the future of realistic systems using this method.

If you don't want to follow this, don't. If you want to follow parts of it but you don't like other parts, that's cool too. I just want the projects I work on to be consistent.


Welcome to my website! I like to program.