API Dependencies Pattern
I'm not sure if I invented this Pattern, but so far I've never heard anybody else talking about API Dependencies. Similar to code dependencies, modules and such, API Dependencies are a pattern to manage what APIs a service depends on.
Problem
When building backend or frontend applications, they rarely work in isolation. More often than not, they depend on other services to provide data or functionality. For example, a frontend application might depend on a backend API to provide data, or a backend service depends on other (micro) services or 3rd party APIs.
But how do we manage these dependencies as of today?
I've came to realize that most of us don't manage them at all. Instead, we're building these dependencies implicitly into our code, e.g. by integrating an SDK, or by using fetch to call an API.
This is problematic for a number of reasons:
- It's hard to see what APIs a service depends on
- It's hard to impossible to understand the request chain, e.g. which API dependencies are involved in a request
- It's hard to monitor the quality of service of implicit API dependencies
- It's hard to test, because we can't easily mock API dependencies if we're not aware of them
- It's hard to change, because we don't know what code is affected by a change in an API dependency
- We don't know for sure where in the code an API dependency is used, so we can't easily remove it
The list is probably not complete, but you get the idea of the problem. I think it's actually a pretty big problem, but developers seem to have accepted it as a fact of life.
To me, "thinking in API Dependencies" is a mindset change. We have dependency management for code, so why not for APIs? We use npm, go modules, Crate, etc. to manage code dependencies, so why do we manually manage API dependencies? Our code could be so much cleaner and easier to understand if we would manage API dependencies explicitly.
Solution
The solution is to manage API dependencies explicitly.
At the very beginning I thought that a simple apis.json
file would be enough,
similar to package.json
or go.mod
.
However, I quickly realized that API dependencies are much more complex to handle than code dependencies. When integrating APIs, we have to deal with authentication, different environments, signing requests, dynamically injecting headers, handling errors and so on.
So I came up with the idea of defining API dependencies using TypeScript. It's a very powerful and flexible language that allows us to define and configure API dependencies in a type-safe way. It's widely used among frontend and backend developers, so most people should be familiar with it. In addition, we can use TypeScript to add custom logic like request signing, error handling, or dynamically injecting headers.
Example
First, let's add our API dependencies.
import { introspect } from '@wundergraph/sdk';
const countries = introspect.graphql({
apiNamespace: 'countries',
url: 'https://countries.trevorblades.com/',
});
const weather = introspect.graphql({
id: 'weather',
apiNamespace: 'weather',
url: 'https://weather-api.wundergraph.com/',
introspection: {
pollingIntervalSeconds: 5,
},
});
configureWunderGraphApplication({
apis: [countries,weather],
});
We introspect two GraphQL APIs and add them as API dependencies. We could also add REST or SOAP APIs, but let's keep it simple.
One problem we might be running into when implementing the API dependencies pattern is naming conflicts. When we're merging multiple APIs into a single unified API, we might run into naming conflicts.
In our case, we've decided to use GraphQL as the common denominator, which means that we translate all non-GraphQL APIs into a GraphQL Facade and merge them into a unified GraphQL API. Doing so, we very easily run into naming conflicts. If you want to learn more about this problem and the solution, check out the section on the API Namespacing Pattern.
Once our API dependencies are configured, we're ready to use them in our application, either by using GraphQL or the TypeScript API ORM.
# .wundergraph/operations/CountryWeather.graphql
query (
$continent: String!
# the @internal directive removes the $capital variable from the public API
# this means, the user can't set it manually
# this variable is our JOIN key
$capital: String! @internal
) {
countries_countries(filter: { continent: { eq: $continent } }) {
code
name
# using the @export directive, we can export the value of the field `capital` into the JOIN key ($capital)
capital @export(as: "capital")
# the _join field returns the type Query!
# it exists on every object type so you can everywhere in your Query documents
_join {
# once we're inside the _join field, we can use the $capital variable to join the weather API
weather_getCityByName(name: $capital) {
weather {
temperature {
max
}
summary {
title
description
}
}
}
}
}
}
This is a GraphQL-defined JOIN between the countries
and weather
API.
As an alternative to using the Query language, we can achieve the same result using the TypeScript API ORM.
// .wundergraph/operations/CountryWeather.ts
import {createOperation, z} from '../../generated/wundergraph.factory';
export default createOperation.query({
input: z.object({
continent: z.string(),
}),
handler: async ({ input, graph }) => {
const country = await graph.from('countries').query('countries').where({
filter: {
continent: {
eq: input.continent,
}
}
}).exec();
return Promise.all(country.map(async (country) => {
const weather = await graph.from('weather').query('getCityByName').where({
name: country.capital || '',
})
return {
...country,
weather,
}
}));
},
});
As you can see, both approaches are very similar. The pure GraphQL approach is more declarative and less verbose, while the TypeScript approach is a bit more verbose but also more flexible.
At the end of the day, API dependency management gives you full transparency over what APIs your service depends on. All requests are routed through a single Gateway, independent what approach you're using, GraphQL or TypeScript ORM. This means that you get full observability into each dependency and can easily monitor the quality of service.