I'm looking for a recommendation for the endpoint design for a particular rest webservice use case.
We have a basic API to enroll a particular user into a particular course:
PUT /rest/courses/{courseId}/enroll/{studentId}
This works well enough, and is straightforward.
Often, however, we have N users that we want to enroll into the same course, where N might be from 1 to 30. I see two likely options to accomplish this bulk enrollment:
Implement a separate endpoint, as follows:
PUT /rest/courses/{courseId}/enroll
...and place the N student IDs in the body of the request.
Option 1 seems not-so-efficient. Option 2 smells a bit off (and doesn't seem right for a PUT, but neither for a POST). Do either of these options seem clearly better (more "restful") to you? Or is there a clear third, better option?
Thanks!
With PUT /rest/courses/{courseId}/enroll/{studentId}
- it is, as you say, straightforward and will work fine, but given that REST is REpresentational State Transfer, one question to always ask yourself with REST is - what is the resource state that is represented at that URI?
e.g. if a client was to call GET /rest/courses/{courseId}/enroll/{studentId}
, what resource representation would be returned? Would it just be the student, or would it be some detail of their enrollment in that course? (e.g. including enrollment date)? Does it make sense to call GET on a resource with the verb 'enroll' in it? Can the same student enroll in the same course multiple times (i.e. if they repeat a course in a later semester?)
Another possibility is:
POST /rest/courses/{courseId}/enrollments
Body:
{
"studentId" : xyz
}
this would return status code 201 Created, and cause a resource to be created at a location, with the Location header set to that URI, e.g.
/rest/enrollments/{enrollmentId}
If you did another POST with the same StudentId, your business logic would check if that was permitted or not, e.g. given the semester of the enrollment, and return 400 if the request is invalid, and 201 Created with the 2nd Enrollment if it was valid.
This convention would also allow you to define:
GET /rest/courses/{courseId}/enrollments
Which could return a list of all the enrollments in that course, and
GET /rest/enrollments
Which could return a list of all enrollments across all courses (if that was helpful).
To get back to your original question, if your URI is expected to be a 'collection' resource, technically a PUT would be expected to replace the whole collection - as it is supposed to semantically mean the client is saying "Here is the representation I would like stored at this resource URI" - so as you guessed, it doesn't smell right for a reason.
POST, however, does not have such a constraint, and in fact is quite flexible. You may define your API to permit the BODY to contain multiple instructions that result in creating multiple resources, e.g.
POST /rest/courses/{courseId}/enrollments
BODY
{
students : [
{
studentId : 100
},
{
studentId : 101
}
]
}
However there are a few complexities that are introduced by permitting this:
Your question mentioned that you thought it didn't seem RESTful to submit multiple Students to the .../enroll
endpoint even with a POST. This is partially, I think, because enroll
(and enrollment
) sound like they only relate to a single student. The alternative is to define a new resource - bulkEnrollment
. e.g. While
POST /rest/courses/{courseId}/enrollments
Would mean 'create an enrollment using the POST data', and only accept a single student, a new resource at:
POST /rest/courses/{courseId}/bulkEnrollments
Would mean 'create a bulk enrollment using the POST data' and accept multiple student ids. The bulk enrollment itself becomes an entity you track and it's resource representation could then be retrieved at:
GET /rest/bulkEnrollments/{bulkEnrollmentId}
The representation would list all the enrollments created by that specific bulk operation, including perhaps some metadata about the bulk enrollment itself, including the authenticated user who did it, and when they did it.
Whether or not you expose a bulkEnrollment
resource, it may also be worth considering introducing hypermedia to your representations - e.g. each of the entries in the collection of enrollments could contain a hypermedia link to the resource representing the individual enrollment (e.g. /rest/enrollments/{enrollmentId}
, sometimes known as a self
link) and each enrollment could contain a hypermedia link to the resource representing the student (e.g. /rest/students/{studentId}
).
In fact, hypermedia (e.g. Hal) is the most RESTful technique you can adopt - far more important that the specific verbs/nouns or structures you use in your endpoints. By only letting the client interact with endpoints that it has discovered by following links previous requests, your API can self-describe the relationships between resources. It also gives you a lot of flexibility as to the actual URI endpoint structure changing over time. But most importantly, it allows you to signal the client as to which operations are actually permitted given the current state of the resource. The concept is known as HATEOAS (the example uses Xml, but Hal can help with the same concepts in Json) and is required for a 'level 3' REST API according to the Richardson Maturity Model for REST APIs.