Search code examples
restrestful-architecturerestful-urlapi-designhttp-verbs

webservice standard approaches for creating multiple associations


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:

  1. Call the above endpoint N times
  2. 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!


Solution

  • What is a resource representation?

    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).

    PUT to collections

    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 multiple records in a single request

    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:

    • Discoverability of created resources - a response can only contain a single Location header, so the client can't find out about all of the created enrollments. You would have to include a list of URIs to enrollments in the response body from the POST, rather than using the Location header
    • Handling partial failures - how do you indicate if adding one student failed, but the other succeeded? You would have to have a body containing error messages, which also isn't RESTful as ideally you should be using 400 or 500 for error conditions... OR you would have to handle the bulk addition atomically, and if any fail, ensure none have been processed (e.g. database rollback)
    • You may need to define a limit on the number of records in the submission, or at least the payload size of the body, lest you open up your API to risk of the Slow Http Attack depending on how long it takes to process each record. One solution for this might be to make use of 202 Accepted and process the request asynchronously - but this assumes you have backend infrastructure to support asynchronous processing.

    A new type of operation - a new resource?

    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.

    Hypermedia/HATEOAS

    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.