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!