restasp.net-web-apiapi-design

REST API design: Grouping together multiple POST or PUT calls?


I have a REST API with the usual sort of structure

/customers                 // get a list of all customers
/customers/22              // get customer with the ID 22
/customers/22/orders       // get all orders for customer 22
/customers/22/orders/156   // get order 156 for customer 22

I have simple DTOs for Customer and Order. The orders aren't part of the customer DTO. You don't necessarily want a list of all the customer's orders every time you retrieve the Customer object. So if you do want the Customer object and a list of all their reports, you make two calls to the API.

But a Customer object also has several collections of very simple child objects, e.g. telephone numbers, addresses, etc. I have made these part of the Customer DTO, and I haven't provided endpoints for the child records.

For example, GET /customers/22 might get you

{
  "id": 22,
  "forename": "Jack",
  "surname": "Smith",
  "telephones": [
    {
      "id": 45,
      "type": "mobile",
      "number": "[phone number here]"
    },
    {
      "id": 46,
      "type": "home",
      "number": "[phone number here]"
    }
  ],
  "emails": [
    {
      "id": 226,
      "type": "personal",
      "number": "[address here]"
    },
    {
      "id": 242,
      "type": "work",
      "number": "[address here]"
    }
  ],
  "addresses": [
    {
      "id": 35,
      "type": "home",
      "house": "24",
      "street": "Hyacinth Avenue",
      "town": "London"
    },
    {
      "id": 7,
      "type": "work",
      "house": "54",
      "street": "Hydrangea Road",
      "town": "Nottingham"
    }
  ]
}

All of the child objects have their own unique identifier.

I'm not completely happy with the way I'm handling updates to a Customer record. I accept

POST /customers/22

And in the body I expect the complete Customer object, including all of the child records. The server works out which (if any) child records have been created, updated, or deleted, and makes the appropriate changes on the database.

There are a couple of problems with this.

  1. If you just want to change something on the main client record (e.g. the spelling of the surname) you have to submit all the child records as part of the update request, which is a nuisance.

  2. If you submitted the following

POST /customers/22

body:

{
  "id": 22,
  "forename": "Jack",
  "surname": "Smyth"
}

Then as well as updating the surname, the API would interpret that as you wanting to delete all of the telephone numbers, e-mail addresses, and addresses. That's stated clearly in the API documentation, but it feels a bit like a disaster waiting to happen.

I'm thinking it would be better to treat the child objects in exactly the same way as any other child resource, and have separate URIs for each, e.g.

/customers
/customers/22
/customers/22/telephones
/customers/22/emails
/customers/22/addresses

and accept the usual GET, POST, and PUT on each.

So if you wanted the "complete" Customer record, you would have to make four calls to the API. That's not ideal, but I'm OK with it if it makes the API clearer and less prone to accidents.

The other implication of that is that it puts the onus on the client to work out what inserts, updates, and deletes are needed if an end user changes multiple things in a client record all at the same time, rather than letting the API work all that out for you. I'm OK with that, though.

The final issue with changing the design is that currently, if a user were to update the surname, add a telephone number, delete an e-mail address, and update a street address, all that would be sent to the API in a single call, and on the database, all the updates would be done in the same transaction. We keep track of all changes to customer records using SQL Server system-versioned tables. The ability to view a history of changes to a customer record is a very important feature of the system. We call it the Customer's timeline.

If I were to change to the new design, instead of one event in the Customer's timeline, they would see four smaller events one after the other. From the point of view of an end user, they may have made all those changes together, so they would expect to see a single timeline event that includes all the changes.

Because of the way that SQL Server system-versioned tables work, I can't set the "ValidFrom" value on the records manually. If I want them to all have the same ValidFrom date, they have to be all within the same database transaction.

I could maybe handle that later, e.g. when the user queries a Customer's timeline, I group together any events that are within a couple of seconds of each other, but that feels a bit woolly.

Is there a common way of allowing a client to bundle multiple API calls into a single call, to indicate that they should all be processed as part of the same transaction?


Solution

  • You could create a BATCH Endpoint. However there are some disadvantages

    In Short:

    Advantages:

    Ensures atomicity and consistency across multiple operations.

    Lets the server handle inserts, updates, and deletes in a single call, avoiding multiple round-trips.

    Simplifies the client-side logic since all operations are bundled.

    Disadvantages:

    Adds complexity to the API.

    Requires careful documentation, as each action needs clear validation and error handling.

    How could that look like?

    You call it something like this: POST /customers/22/batch

    Your request body can look like this

    {
      "operations": [
        { "action": "update_customer", "data": { "surname": "Doe" } },
        { "action": "add_telephone", "data": { "type": "mobile", "number": "0815" } },
        { "action": "delete_email", "id": 123},
        { "action": "update_address", "id": 1, "data": { "street": "Some street" } }
      ]
    }
    

    The Server will ensure all changes get handled or not at all

    For more insights you may want to visit: https://tyk.io/blog/api-design-guidance-bulk-and-batch-import/

    They explained it neatly it got used as source for my studies back in training