API request bodies design standards

Hi @wootmoot ,

I’m curious to know :slightly_smiling_face:

why does some POST apis have request fields on the request wrapper e.g. CreateCustomer, CreateSubscription vs some apis use model wrapper around request wrapper e.g. CreateCard, CreateOrder, CreateBooking

Similarly, for PUT apis, some apis use request fields on the request wrapper e.g. UpdateCustomer vs some use model wrapper around by request wrapper e.g. UpdateSubscription?

Thanks,
Harsh

You’re asking a really good (and possibly quite interesting question). I will forewarn, that there is more reasons for this than I’m able to go into, but I hope I can make clarify a little bit why things are different.

Part of this is historical and another part of this is pattern matching as certain APIs were created/released.

Payments and Customers in Connect v2 have been around a bit longer than other APIs, so when they first came out (specifically in Connect v2) decided to have top-level write fields on the request (I believe you refer to this as “on the request wrapper”) vs nesting the fields under a key and mapping to a shared model (what you’re calling “model wrapper”, but we could also call this enveloped).

At some point in the releasing of Connect v2 APIs, it was thought that following a more ORM-style pattern where you could share the model in various areas would be advantageous to developers. So APIs were following this “enveloping” pattern and sharing the same model where possible.

Unfortunately, this has resulted in a mixture of request patterns across APIs.

The reason that I say it is part historical and part pattern matching as well, is that Subscriptions API, which is newer compared to some others, decided to follow the patterns found in Payments API. This was in part because of how they anticipate developers working with both APIs, they thought it was be easier to adopt if it followed a similar pattern.


I don’t know how helpful all of this is other than giving a little explanation as to why things are the way that they are. But it would be really helpful to know which pattern you find easier to understand or implement. Do you have a preference between APIs in terms of how request fields are being handled?

1 Like

Thank you for the reply and thank you very much elaborating on it. You interpreted my question exactly the way I thought. Your answer was straight to the point and helped a lot.

For your question:

Usability and Implementation: In my opinion, as a consumer of the API, I would prefer envelope pattern easy to use. It brings clarity to the design.

Which pattern would I prefer: As a designer/producer of these apis, I think both have their own tradeoffs.

Top-level write fields - This approach allows to clearly isolate requirements between different HTTP methods request bodies. Meaning, its more flexible to design each endpoint request independently of the other endpoints. But increases cost of maintaining such api contract. Additionally, looking at the all the apis request bodies having top level write fields, its possible the clients might create their own models and relations in a way we never would have thought of. May be this might define the complexity of migrating to a newer version for the clients using these contracts.

Envelope Pattern - This helps to overcome the maintenance problem but now endpoints are tightly coupled with a different request envelope reusing the same model. E.g. shipping_address:Address and billing_address:Address. Here, Address model is being reused in two envelopes (possibly in same or different endpoints). Although address in my experience is comparatively stable model. We typically want to have same changes applied everywhere. But, things start becoming complex when there are complex business envelop which have nested envelops within them.

Need to think carefully while designing such apis. e.g. Square: Retrieve Customer endpoint response containing Customer model might have values for all the fields e.g. group_ids that we might not want clients to add/remove with Update Customer endpoint using same Customer model in the request. So probably would want to make group_ids read only in this case and allow group ids to be updated via customer/{customerId}/groups/{group_id} endpoints only. (I don’t have deeper understanding of square system here, but trying put things in perspective here)

I am sure there might be few other important challenges which I haven’t mentioned.

Having said that, I would prefer envelope pattern because its much easier to learn and correlate resources - this would push myself to build more clean request response models and identify issues at the design phase due to the coupling it creates.

I hope I was able answer to your question. :slightly_smiling_face:

Thanks,
Harsh

What if it followed an envelope style pattern, but nested in the envelope was only the fields that are writeable in that endpoint request?

This Create Payment request

{
  "amount_money": {
    "currency": "USD",
    "amount": 9500
  },
  "idempotency_key": "f3c1c71f-6703-465d-8219-387912020c14",
  "source_id": "cnon:card-nonce-ok",
  "autocomplete": false,
  "order_id": "b59cmFTMltVTycU3bj5PsIiouaB",
  "customer_id": "VT5X2WXW1MRTXE4SQ5HFBH4ZGC"
}

Becomes this Create Payment request

{
  "idempotency_key": "f3c1c71f-6703-465d-8219-387912020c14",
  "payment" : {
    "amount_money": {
      "currency": "USD",
      "amount": 9500
    },
    "source_id": "cnon:card-nonce-ok",
    "autocomplete": false,
    "order_id": "b59cmFTMltVTycU3bj5PsIiouaB",
    "customer_id": "VT5X2WXW1MRTXE4SQ5HFBH4ZGC"
  }
}

I know the difference isn’t that big, but its kind of a hybrid between the two patterns. But if you can imagine, its more like we’re masking certain fields in the shared model to only have the fields that are relevant to that particular request being exposed.

Part of the reasoning here being that fields that are relevant in the Create might not be relevant in the Update. This is especially the case if the field is set in a Create, but after that is immutable (basically a write-once field).

That is a little harder to express in a validation on shared models, but if you’re not entirely sharing the model between the Create and Update, you can sort of sidestep that issue.

Your feedback is really helpful and I will certainly be sharing it with our API design team :slight_smile:

1 Like