December 18, 2024

Simplify OData API Orchestration with Apollo Connectors

Michael Watson

Michael Watson

OData (Open Data Protocol) is a standardized protocol for building and consuming RESTful APIs, enabling developers to query and manipulate data using a uniform, SQL-like syntax across diverse platforms. While it simplifies data access with features like filtering and pagination, developers often struggle with its complexity in implementation, over-fetching or under-fetching data, and scaling for real-world applications.

These complexities often arise from the need for developers to coordinate API requests across business domain entities to deliver a new feature. OData introduced features that enable relationships within the API, but this still required application developers to have to understand this complexity to build their new feature. The complexity of making those relationships with foreign keys was more difficult than just writing some client code to orchestrate requests across the entities. This leads to bloated client code or additional service layers like backend-for-frontend (BFF).

It was never supposed to be this complicated! Apollo Connectors makes orchestrating your REST API calls easy. You simply connect your REST endpoint to portions of your schema and a plan will be built and executed based on the client’s request. Let’s see how that works using the OData Basic/Advanced tutorials and the API they provide: https://services.odata.org/V4/TripPinServiceRW.

Using your OData API as a source

Apollo Connectors enables you to connect to any REST API by writing a GraphQL schema with metadata for the details of your API. You can do this using the @source directive and provide a baseURL for where your OData API lives:

extend schema
  @link(url: "https://specs.apollo.dev/federation/v2.10", import: ["@key"])
  @link(url: "https://specs.apollo.dev/connect/v0.1", import: ["@source", "@connect"])
  @source(
    name: "TripPin"
    http: { 
      baseURL: "https://services.odata.org/V4/" 
      headers: [{ name: "Authorization", value: "Bearer {$config.apiKey}"}]
    }
  )

Connecting an Individual Entity by ID

OData has the concept of an entity based on a single ID and our first step in exposing these entities in the connector we’re building. You’ll want to define the entities you want to orchestrate requests across. First you’ll need to define the shape you want to return, which is typically the entity returned by your ODataController:

type Person {
  id: ID!
  firstName: String
  lastName: String
  emails: [String]
}

Now we need to connect the defined Person to our API endpoint using the @connect directive and expose our GET endpoint. We typically place read-only functionality in the root Query of our schema. The OData tutorial provides a People route, but it’s important to note that we don’t want to just use /TripPinServiceRW/People when you have versioned your entity models in your OData API. For this scenario, you should be using a specific $schemaversionto the url structure:

type Query {
  person(id:ID!): Person
    @connect(
      source: "TripPin"
      http: { GET: "(S(unfmciiartlvloowd54bozof))/TripPinServiceRW/People('{$args.id}')" }
    )
}

With our schema connected, we need to provide a selection of the data coming from our API and how to transform the data to the shape we defined. If you opened up https://services.odata.org/V4/(S(unfmciiartlvloowd54bozof))/TripPinServiceRW/People(‘russellwhyte’), you’ll see the full shape of the People entity. The Username field is the id and I’ll want to rename the fields to use camel case. Below is an example of the schema next to the actual results returned from the API so you can see how they map together:

On the left is a GraphQL schema that defines the Apollo Connector for the /TripPinServiceRW/People('') endpoint

On the right is the shape of the response returned from the /TripPinServiceRW/People('') endpoint

Now we have a full defined connector for our Person and the last step is to define it as an entity by setting entity: true:

type Query {
  """
  Get a Person
  http://services.odata.org/V4/(S(unfmciiartlvloowd54bozof))/TripPinServiceRW/People('russellwhyte')
  """
  person(id:ID!): Person
    @connect(
      source: "TripPin"
      http: { GET: "(S(unfmciiartlvloowd54bozof))/TripPinServiceRW/People('{$args.id}')" }
      selection: """
        $.value {
          id: UserName
          firstName: FirstName
          lastName: LastName
          emails: Emails
        }
      """
      entity: true
    )

And that’s it! The nice thing here is that this pattern can be applied to any entity you have defined in your OData API!

Connecting an Entity Collection

OData controllers provide basic list functionality for entities defined and we can connect to that endpoint just like we did for the entity, except omitting entity: true and the arguments:

On the left is a GraphQL schema that defines the Apollo Connector for the /TripPinServiceRW/People endpoint

On the right is the shape of the response returned from the /TripPinServiceRW/People endpoint

Adding pagination for Entity Collection

OData provides system query options that can be used for pagination or filtering. We can quickly expose the pagination rules by adding skip and take to our arguments and mapping that to OData’s $top and $skip:

type Query {
  """
  Get a list of people that can be paginated
  http://services.odata.org/V4/TripPinService/People?$top=1&$skip=2
  """
  people(take: Int = 3, skip: Int = 0): [Person]
    @connect(
      source: "TripPin"
      http: { GET: "(S(unfmciiartlvloowd54bozof))/TripPinServiceRW/People?$top={$args.take}&$skip={$args.skip}" }
      selection: """
        $.value {
          id: UserName
          firstName: FirstName
          lastName: LastName
        }
      """
    )
}

Creating an Individual Entity

Now that we’ve seen how to expose reading an entity, we need to add the “create” functionality for our entity as a POST request. This is going to look very similar to how we read an entity, but we’ll need to create argument inputs that are mapped into the body we define in @connect:

type Mutation {
  """
  Create a Person
  http://services.odata.org/V4/TripPinService/People
  """
  createPerson(username: ID!, firstName: String, lastName: String): Person
    @connect(
      source: "TripPin"
      http: { 
        POST: "(S(unfmciiartlvloowd54bozof))/TripPinServiceRW/People/"
        body: """
          $args {
            Username: username
            FirstName: firstName
            LastName: lastName
          }
        """
      }
      selection: """
        $.value {
          id: UserName
          firstName: FirstName
          lastName: LastName
        }
      """
    )
}

Updating an Individual Entity by ID

Updating an entity looks almost identical to creating an entity, but in this case we have a PATCH request instead of a POST:

type Mutation {
  """
  Update a person
  http://services.odata.org/V4/TripPinService/People('miathompson')
  """
  updatePerson(username: ID!, updates: PersonUpdates): PersonUpdatesResponse
    @connect(
      source: "TripPin"
      http: { 
        PATCH: "(S(unfmciiartlvloowd54bozof))/TripPinServiceRW/People('{$args.username}')"
        body: """
          $args {
            Username: username
            FirstName: updates.firstName
            LastName: updates.lastName
            Emails: updates.emails
          }
        """
      }
      selection: """
        success: $(true) 
        message: $("User deleted")
      """
    )
}
input PersonUpdates {
  firstName: String
  lastName: String
  emails: [String]
}
type PersonUpdatesResponse {
  success: Boolean
  message: String
}

You can either create a generic update field for each entity (i.e. updatePerson) where you update any fields in a grouped input, or you can have more granular names that describe what is being updated vs a generic update (i.e. updatePersonName):

type Mutation {
  """
  Update a person
  http://services.odata.org/V4/TripPinService/People('miathompson')
  """
  updatePersonName(username: ID!, firstName: String, lastName: String): PersonUpdatesResponse
    @connect(
      source: "TripPin"
      http: { 
        PATCH: "(S(unfmciiartlvloowd54bozof))/TripPinServiceRW/People('{$args.username}')"
        body: """
          $args {
            Username: username
            FirstName: updates.firstName
            LastName: updates.lastName
          }
        """
      }
      selection: """
        success: $(true) 
        message: $("User deleted")
      """
    )
}
type PersonUpdatesResponse {
  success: Boolean
  message: String
}

Invoking a Singleton

OData provides a capability where you can expose singletons on the models you define in your API. This is typically used for “experience” APIs that provides some functionality for an entity or model. Depending on what your Singleton does will determine if you want it to be a “read” operation (in the Query) or a “write” operation (in the Mutation). For the tutorials, they expose a GetNearestAirport that returns the closed airport based on a latitude and longitude. We’ll put this read functionality into our Query for this scenario and it will look very similar to our Person connections:

On the left is a GraphQL schema that defines the Apollo Connector for the /TripPinServiceRW/GetNearestAirport endpoint

On the right is the shape of the response returned from the /TripPinServiceRW/GetNearestAirport endpoint

Connecting it all together

By combining OData with Apollo REST Connectors, you can quickly build an API orchestration layer that leverages everything in OData but leaves the complexity behind. We covered a pattern on how you can connect entities you have defined in your OData APIs and you can apply this pattern to any OData Entity.

If you want to get started with Apollo Connectors, head over to our quickstart documentation and then you can recreate everything in this post. We’ll also be hosting our second episode of Connectors Live on January 15, 2025 where we’ll build out this OData orchestration layer live from scratch to hosted instance. We’re working on the registration link for the next episode and will update here once it’s up. Until then, you can also checkout our last episode that covers a checkout flow for the Stripe REST API if you want to see more connectors examples.

Happy building! 🛠️

Written by

Michael Watson

Michael Watson

Read more by Michael Watson