Premier API companies, like Stripe, implement an additional expand parameter on all their
endpoints. Generally, nested objects in the Stripe API are not expanded by default and instead are
represented by their id . This parameter gives you ability to inflate the nested objects returned
from the API and get their attributes and also recursively inflate the nested objects within them.
At Pocketflows, we’re building embedded APIs to help VSaaS and
enterprise companies build high-quality marketing tools faster and better. It’s our goal to offer
the best API experience, so we have implemented the expand parameter and wanted to share our
thoughts.
Why have an expand parameter at all? We can see its utility by examining alternative approaches.
Note that GraphQL actually solves many of the following concerns, but that’s a topic for a future
blog post.
One option (that Pocketflows implemented in its MVP) is to expand every nested object fully and
recursively. For example, in Pocketflows, there is a Member which associates both a User and a
Rewards Program and gives . If we take the “expand everything” approach, a GET to
/members/mem_1 would return
{
"id": "mem_1",
"point_total": 1000,
"point_balance": 2000,
"user": {
"id": "u_1",
"phone_number": "+1234567890"
},
"rewards_program": {
"id": "rp_1",
"points_earned_per_dollar_spent": 2,
"point_redemption_value": "0.01",
"points_for_member_class_attendance": 100
}
}
Members would return an array of the
above object and for many of the Members, rewards_programs would be exactly the same object!One final subtlety is that with this approach, you need to be careful about implicit cycles within your object model. If there was a one-to-one relationship between two objects and we expand everything naively, we end up with a situation similar to infinite recursion!
On the opposite extreme, we can choose to expand none of the nested objects. Keeping the example
from above, with Members, making a GET request to /members/mem_1 would return:
{
"id": "mem_1",
"point_total": 1000,
"point_balance": 2000,
"user": "u_1",
"rewards_program": "rp_1"
}
This approach addresses some of the concerns from the “expand everything” approach.
User or Rewards Program , they
need to make a separate API calls to /users/u_1 and /rewards_program/rp_1 .expand parameterThe “expand nothing” approach has many benefits with one major limiting downside: when the API
caller would prefer to have more information about a nested object, they need to make separate calls
to the API. We address this by providing an opt-in parameter: expand. The default behavior of
all endpoints is to “expand nothing” as above, but when API callers want to view nested objects in
the response, they can provide the expand parameter and get the data in the same response.
Continuing with the example, suppose an API caller would like to view a Member’s point total and
would also like their phone number to send them a notification. They can now include the expand
parameter in the GET request: /members/:id?expand[]=user . This returns
{
"id": "mem_1",
"point_total": 1000,
"point_balance": 2000,
"user": {
"id": "u_1",
"phone_number": "+1234567890"
},
"rewards_program": "rp_1"
}
The result selectively expands the User nested object but leaves the Rewards Program unexpanded.
This gives the API caller the requested information about the User (their phone number) while not
giving redundant information about the Rewards Program . There are few tradeoffs here! We’re
minimizing the amount of bandwidth and redundant information while still giving all the relevant
information in one API call. The API caller only needs to provide an extra query parameter.
Now that we have motivated the utility of the expand parameter, we should also clarify the way it
works! expand is an array of strings passed to the query parameters of GET requests and the
request body of POST and DELETE requests.
For GET requests, arrays are encoded with the expand[] scheme. For example, if the intent is to
expand both user and rewards_program , the query parameters should be
expand[]=user&expand[]=rewards_program. This will be interpreted as
expand = ["user", "rewards_program"]
by the API.
Each of the strings should correspond to keys in the response object that have expandable values. In
a Member object:
{
"id": "mem_1",
"point_total": 1000,
"point_balance": 2000,
"user": "u_1",
"rewards_program": "rp_1"
}
"user" and "rewards_program" are valid values.
In addition, recursively nested objects can also be expanded with a dot notation. For example, an
Event in Pocketflows has an associated Member. If we would like to see the associated Member’s
phone number, we would expand the "member" key and then the "user" key in a Member. The API
call might be POST with
{
"expand": ["member.user"]
}
returning the result
{
"id": "ev_1",
"member": {
"id": "mem_1",
"user": {
"id": "u_1",
"phone_number": "+1234567890"
},
"rewards_program": "rp_1"
},
"event_name": "user.book"
}
Note that "member.user" automatically implies the expansion of "member" . It is not necessary to
pass both "member" and "member.user" .
For endpoints that return multiple objects or expanding nested objects in arrays, the expand
parameter needs to pass the next relevant key, skipping any potential array indexing. Again, the
rule for matching keys must be followed. If you make a GET to /members, you receive the result
{
"members": [
{
"id": "mem_1",
"user": "u_1"
},
{
"id": "mem_2",
"user": "u_2"
}
]
}
To expand the "user" key for all of these, pass ?expand[]=members.user to the query to receive:
{
"members": [
{
"id": "mem_1",
"user": { "id": "u_1", "phone_number": "+1234567890" }
},
{
"id": "mem_2",
"user": { "id": "u_2", "phone_number": "+0987654321" }
}
]
}
To prevent excess querying that stems from requesting too many layers of nested objects, we can
restrict the amount of chaining allowed by the expand parameter. Stripe chooses a value of 3;
providing deeper nesting should result in an error. At the moment, Pocketflows makes no such
restrictions! Arbitrary depth can be provided to our API with no errors.
If the API caller passes a key that’s not valid on the object, either because the key is misspelled or the key does not correspond to an expandable object, this should generate an error from the API.
At Pocketflows, we’re building our API with Ruby on Rails. Check out part two to learn tips and tricks from our sample implementation!
The expand provides flexibility to API callers while providing sensible and minimal defaults for
all endpoints — unless otherwise specified, the amount of data sent is minimal, conserving bandwidth
and database utilization. Implementing the expand parameter is relatively straightforward, and we
hope you also include it in your API!
Are you looking to build marketing tools in your VSaaS or consumer-facing business? At Pocketflows, we’re creating embedded APIs to build high-quality marketing products. We’re incorporating the best API practices and creating the best developer experience. If you’re interested in learning more, click the button below to schedule time with us!