I am working on the backend of a web application that exposes a REST-like API to the frontend of the application.
Currently, I am exposing a "Users" resource, where each user can be represented in JSON notation as:
{
"id": "asdfzxcv",
"name": "Morris",
"preferences": {
"dark_mode": true,
"tags": [ "tagA", "tagB" ]
}
}
I currently expose this user at the endpoint GET /api/v1/users/<user-id>
.
Now, I have been given the requirement that the frontend should be able to:
- Submit a list of tags to be added to the user's preferences
- Submit a list of tags to be removed from the user's preferences
- Submit a list of tags to replace the user's preferred tags
Originally, I was thinking the client could do PUT (or DELETE) /api/v1/users/<user-id>/preferences/tags/tagA
, but this would entail making multiple requests to add/remove multiple new preferences, which would be very wasteful.
How should I design the URL paths to handle this case?
One possible solution is to use query parameters to change the behavior:
POST /api/v1/users/<user-id>/preferences/tags
(default action: update/merge)POST /api/v1/users/<user-id>/preferences/tags?action=replace
POST /api/v1/users/<user-id>/preferences/tags?action=delete
Or I could have 3 separate URLs:
POST /api/v1/users/<user-id>/preferences/tags
(default action: update/merge)POST /api/v1/users/<user-id>/preferences/tags/replace
POST /api/v1/users/<user-id>/preferences/tags/delete
1 Answer 1
If you want to be "more REST-ish", you can model tags
as a single "collection" resource, and support multiple HTTP methods on it:
- The resource at
/api/v1/users/XXX/preferences/tags
represents the list of tags for userXXX
GET /api/v1/users/XXX/preferences/tags
returns a representation of all the current tags for that userPUT /api/v1/users/XXX/preferences/tags
replaces the list with a completely new listPOST /api/v1/users/XXX/preferences/tags
adds a tag to the listDELETE /api/v1/users/XXX/preferences/tags
removes all tags from the listPATCH /api/v1/users/XXX/preferences/tags
allows changes to the collection
It's that last one which gives you the power to do multiple adds or deletes in one request, even in the same request. All you need is to pick a representation for the changes, which could be something like this:
{
"add": ["foo", "bar"],
"remove": ["tagA"]
}
(You could also use a standard format, like JSON Patch but it seems less suitable for managing lists.)
-
Thanks for the thoughtful answer. I'm a bit surprised that there isn't a more general-purpose JSON-based spec for submitting modifications to nested data structures. Maybe something like
{ "tags[add]": ["foo", "bar"], "tags[remove]": ["baz"] }
. Although this entails some extra parsing of attribute names.shadowtalker– shadowtalker2021年02月17日 17:35:11 +00:00Commented Feb 17, 2021 at 17:35 -
@shadowtalker Well, JSON Patch is exactly that, but it relies on JSON Pointer, which can only reference array elements by position, so it's a bit "clunky" for the current case. A variant using a more powerful selection language like JMESPath might work, but it will probably be easier all around to implement something bespoke but simple, as long as you test and document it carefully.IMSoP– IMSoP2021年02月17日 17:45:22 +00:00Commented Feb 17, 2021 at 17:45