API Concepts

In this document, we will be looking at API concepts that exist and should be followed by endpoints. We also describe why these concepts exist so that developers can use them at their own discretion.

Expanding responses allow us to include relational information on a resource without loading it by default.

In general, endpoints should expose the fewest fields that will make the API usable in the general scenario. Doing one SQL request per API request is a good rule of thumb. To return information on a bounded relationship, endpoints should rely on the expand parameter. To return an unbounded relationship, it should be another endpoint.

To take an example, let's talk about the projects list endpoint. A project belongs to an organizations but could be on multiple teams.

By default, here's what the project endpoint should look like

Copied
GET /api/0/projects/{project_slug}/
{
  "id": 5,
  "name": "foo",
  ...
}

To display information about a bounded relationship, a user should be able to use the expand parameter. This is generally only true for 1:1 relationships.

Copied
GET /api/0/projects/{project_slug}/?expand=organization
{
  "id": 5,
  "name": "foo",
  "organization": {
    "slug": "bar",
    "isEarlyAdopter": false,
    ...
  }
  ...
}

For unbounded relationships, make a separate query. This allows the query to be paginated and reduces the risk of having an arbitrarily large payload.

Copied
GET /api/0/projects/{project_slug}/teams
[
  {
    "id": 1,
		"name": "Team 1",
		"slug": "team1",
  },
	{
    "id": 2,
		"name": "Team 2",
		"slug": "team2",
  }
]

Similar to expanding responses, an API endpoint can also collapse responses. When the collapse parameter is passed, the API should not return attributes that have been collapsed.

To take an example, let's look at the project list endpoints again. A project gets events and hence, has a stats component, which conveys information about how many events were received for the project. Let's say we made the stats part of the endpoint public, along with the rest of the projects list endpoint.

Copied
GET /api/0/projects/{project_slug}/
{
  "id": 5,
  "name": "foo",
  "stats": {
      "24h": [
          [
              1629064800,
              27
          ],
          [
              1629068400,
              24
          ],
          ...
      ]
  }
}

The collapse parameter can be passed to not return stats information.

Copied
GET /api/0/projects/{project_slug}/?collapse=stats
{
  "id": 5,
  "name": "foo",
  ...
}

This is typically only needed if the endpoint is already public and we do not want to introduce a breaking change. Remember, if the endpoint is public and we remove an attribute, it is a breaking change. If you are iterating on an undocumented endpoint, return the minimal set of attributes and rely on the expand parameter to get more detailed information.

APIs often need to provide collections of data, most commonly in the List standard method. However, collections can be arbitrarily sized, and tend to grow over time, increasing lookup time as well as the size of the responses being sent over the wire. This is why it's important for collections to be paginated.

Paginating responses is a standard practice for APIs, which Sentry follows. We've seen an example of a List endpoint above; these endpoints have two tell-tale signs:

Copied
GET /api/0/projects/{project_slug}/teams
[
  {
    "id": 1,
		"name": "Team 1",
		"slug": "team1",
  },
	{
    "id": 2,
		"name": "Team 2",
		"slug": "team2",
  }
]

  1. The endpoint returns an array, or multiple, objects instead of just one.
  2. The endpoint can sometimes end in a plural (s), but more importantly, it does not end in an identifier (*_slug, or *_id).

To paginate a response at Sentry, you can leverage the self.paginate method as part of your endpoint. self.paginate is the standardized way we paginate at Sentry, and it helps us with unification of logging and monitoring. You can find multiple examples of this in the code base. They'll look something like:

Copied
def get(self, request: Request) -> Response:
    queryset = ApiApplication.objects.filter(
        owner_id=request.user.id, status=ApiApplicationStatus.active
    )

    return self.paginate(
        request=request,
        queryset=queryset,
        order_by="name",
        paginator_cls=OffsetPaginator,
        on_results=lambda x: serialize(x, request.user),
    )

The example above uses an offset type paginator, but feel free to use whatever paginator type suits your endpoint needs. There are some existing types that you can leverage out of the box, or you can extend the base class BasePaginator and implement your own.

Help improve this content
Our documentation is open source and available on GitHub. Your contributions are welcome, whether fixing a typo (drat!) or suggesting an update ("yeah, this would be better").