One ORM to map them all
Stefan π
Posted on June 5, 2023
What was a pipe dream for a long time is now a reality.
We're proud to announce the first release of the WunderGraph ORM.
The WunderGraph ORM is a TypeScript ORM that gives you a single unified API to access all your data sources,
from REST, SOAP, GraphQL, SQLite, MySQL, PostgreSQL, and more.
This is a huge step towards our vision of making data and APIs more accessible to everyone.
At the same time, this marks the first time where we worked together with an outside oss contributor.
In that sense, all credits go to Tim Kenndal for making this possible.
Without him, his in-depth knowledge of TypeScript, and his dedication to make this happen, this would not have been possible.
Thank you, Tim! π
What's the WunderGraph universal API ORM all about and how does it work?
Before we dive into a more lengthy explanation, let's take a look at a simple example.
Let's fetch some data about a country, including the capital, and then fetch the weather for that capital city from another API.
import { createOperation } from "../../generated/wundergraph.factory";
export default createOperation.query({
handler: async ({ graph }) => {
const germany = await graph
.from('countries')
.query('country')
.where({ code: 'DE' })
.exec();
const weather = await graph
.from('weather')
.query('getCityByName')
.where({ name: germany.capital || '' })
.select(
'weather.summary.icon',
'weather.summary.description',
'weather.summary.title',
).exec();
return {
germany,
weather,
}
}
});
The first API call fetches data from the "countries" API,
the second one has typesafe access to the response of the first API call und fetches the correlated weather data from the "weather" API.
In addition, we're using the select
method to reduce the response payload, which simultaneously reduces the TypeScript types of the response.
The whole operation is wrapped in a createOperation.query
function call, which uses the TypeScript AST in a compile-time step to generate not just TypeScript models for a generated client, but also a JSON-Schema for the response which will be used to generate an OpenAPI Specification (OAS) as well as a Postman Collection for the resulting API.
After all, did you notice what kind of APIs we're using here?
Is it REST? GraphQL? SOAP? Something else? Does it matter?
Try it out now and see for yourself
Before we deep dive into the details, here's a quick way for you to try it out yourself.
If you're keen to try it out right now, you can do so by following these steps:
npx create-wundergraph-app my-app --example orm
cd my-app
npm i && npm start
This will create a new project using the WunderGraph CLI and start a development server. You can now call the API with the following curl command:
curl http://localhost:9991/operations/country?code=DE
To modify the example, have a look at the .wundergraph/operations/country.ts
file.
How does the WunderGraph API ORM help build composable Applications?
Traditionally, we've been building monolithic applications.
This means that we can call into any part of the application with a function call.
Over time, we've started splitting up our applications into smaller composable parts, (micro)services.
In addition, we've started using SaaS services more and more to outsource parts of our application.
Both trends, Microservices and SaaS, have led to a distributed system architecture.
To communicate with these distributed systems, we're using APIs. However, using APIs is not as easy as calling a function,
and there's a lot of boilerplate involved in calling APIs, especially when we're using multiple APIs from different vendors with different API styles like REST, SOAP, GraphQL, etc.
To solve this problem of making APIs more accessible,
a lot of vendors have started providing SDKs for their APIs. However, SDKs come with a lot of problems which we're trying to solve with this new approach,
the universal API ORM.
Why is this a big deal and how is it better than using SDKs?
So, let's discuss the shortcomings of SDKs and how the WunderGraph ORM solves them.
1. SDKs from different vendors & API styles have different ergonomics
No two SDKs are the same, especially when they are provided by different vendors or the underlying API style is different. In addition, not every API style makes it easy to generate a client, for example, SOAP.
2. SDKs have varying levels of quality
Vendors like Stripe or Twilio provide great SDKs, but not every vendor has the resources to provide a great SDK.
In addition, SDKs are often an afterthought and not the primary focus of the vendor.
Often times, what you get is a client that's been auto-generated from e.g. an OpenAPI specification that's not really that great to use.
3. SDKs are not always available
Not every API vendor provides an SDK.
In addition, not every API vendor provides an SDK for every programming language.
This means that you might have to generate your own SDKs sometimes,
but do we really want to do this?
E.g. when using the fly GraphQL API internally, I had to generate a golang client for it, which wasn't really fun.
I had to download the schema, define operations, and run a code generator to generate a type-safe client.
Imagine doing this for 10 APIs and having to keep everything in sync.
4. SDKs might come with a performance penalty
Every SDK you're installing might come with additional dependencies that you might not need.
If you're using multiple SDKs from different vendors,
you might even end up with multiple versions of the same dependency. This can lead to a bloated bundle size and a performance penalty
e.g. when looking at cold start times.
Additionally, you have very little control over the code internals of the SDK. It might be implemented poorly, but you're stuck with it.
5. SDKs might simply not work in your environment
We've got customers who run their applications behind a corporate firewall with a proxy server.
This means that every SDK needs to be able to work with a proxy server.
If it doesn't allow you to configure or customize the HTTP client, you need to find a workaround.
6. SDKs lock you into a specific vendor
Some vendors might go the extra mile and provide high quality SDKs that are more than just wrapper around their API.
They might provide additional functionality that makes it easier to use their API.
This is great, but also means that the cost to switch to another vendor is higher.
7. SDKs might make testing harder or even impossible
When the API calls are hidden behind an SDK, it's harder or even impossible to mock the API calls.
What doesn't sound like a big deal at first, can become a real problem when you're trying to apply test-driven development.
You don't want to make real API calls, especially for mutations, when running your tests, right?
Imagine you're using 10 different SDKs from 10 different vendors.
Do you want to figure out 10 custom ways to mock the API calls? Sounds like fun!
8. SDKs might introduce security risks or vulnerabilities
When using an SDK, you're trusting the vendor to provide a secure client.
Does the SDK use the latest version of the HTTP client?
Does it follow the latest security best practices?
How can you actually audit this?
Is an SDK not just a black box that most of us are blindly trusting?
9. SDKs might have different ways and formats for logging
When using multiple SDKs from different vendors,
do they always allow you to pass a custom logger?
If not, will we end up with multiple logging formats and inconsistent logging levels?
How can we use centralized logging when every SDK does it differently?
10. SDKs might have different ways of handling Errors
Should an SDK throw an exception or return an error object?
Ask 2 SDK vendors, and you get 3 different answers.
11. SDKs complicate Observability
Ideally, we want to be able to trace every API call we're making.
If a client request comes into our system that triggers three origin API calls to different vendors in the backend, we want to be able to see exactly how long each API call took, where the request pipeline was blocked,
and if any of the API calls failed, we want to know which one and why.
If we're using SDKs, we might not be able to instrument all of them in a consistent way, resulting in the inability to trace the whole request and surface the information in our unified observability tool.
12. SDKs introduce inconsistent API key management
When using third party APIs, we often need to pass an API key or secret to authenticate against the API. If we're dealing with multiple APIs from different vendors,
we can be sure that they don't all use the same pattern for handling API keys.
13. SDKs obfuscate which application is using an API key
Imagine a situation where in a larger organization multiple teams share the same API key for a third party API. How can you attribute the API calls to the right team or application?
How do you know at the end of the month which team or application caused high costs?
14. SDKs obfuscate which APIs an application depends on
When using SDKs, we can see that we've installed a dependency in the package manager manifest file,
but how do we know if this is an API dependency and not just a code dependency?
A tool should do one job and do it well.
Package managers like npm are designed to manage code dependencies, not API dependencies.
If we're using SDKs, we're mixing both concerns and obfuscate which APIs our application depends on.
15. SDKs don't usually allow you to reduce the response
As you've seen in the examples, we're using the select
operator to reduce the amound of data we're getting back from the API,
which also modifies the TypeScript types of the response.
const weather = await graph
.from('weather')
.query('getCityByName')
.where({ name: germany.capital || '' })
.select(
'weather.summary.icon',
'weather.summary.description',
'weather.summary.title',
).exec();
We're only really interested in the weather icon, description and title.
As the types of the response are being used for the exposed API of our application (we use them to generate the OpenAPI specification), we don't want to expose the whole response of the API to our users.
With SDKs, we'd have to manually reduce the response or create mappers and manual type definitions to reduce the response.
REST APIs, the dominant API style for publicly available APIs, usually send a lot more data than you might want to expose to your own users, which means that you have to do this for every API call.
If you simply expose the whole response of the API,
you might end up with a bloated API or simply sending way too much data over the wire.
How is the WunderGraph universal API ORM a better alternative to SDKs?
- You get a unified API across all APIs you're using, independent of the vendor or underlying API style
- The universal API ORM is a single open source dependency that's well maintained and you can easily audit it
- Some vendors might not provide an SDK, but you can still use the universal API ORM
- The universal API ORM uses our API Gateway behind the scenes, which is written in Golang and optimized for performance
- As we're using the reverse-API-Gateway pattern, you can use HTTP Proxies for all API calls
- By using the universal API ORM, you're not locked into a specific client or vendor, making it easier to switch providers of an API
- The universal API ORM hooks into our API-Gateway, so you can easily mock API calls during testing for all APIs
- Regarding security, you can audit the universal API ORM and the API Gateway, as both are open source
- You get unified logging and error handling across all APIs you're using
- Another benefit of the reverse-API-Gateway pattern is that you get distributed tracing across all APIs for free
- API keys are managed in a single place, making it easier to audit and rotate them
- WunderGraph introduces the concept of API dependencies, which is distinct from code dependencies, making it easier to see which APIs your application depends on
- The universal API ORM allows you to reduce the response of an API call, so you can expose only the data you want to expose to your users
What's the reverse API Gateway pattern and how is it relevant to the universal API ORM?
In the above section, we've mentioned the reverse API Gateway pattern a couple of times.
Let's take a closer look at what this is and why it's relevant to the universal API ORM.
Let's define what an API Gateway is first and then distinguish between the forward and reverse API Gateway pattern.
What is an API Gateway?
An API Gateway is a single entry point for all API calls.
It's a single server that acts as a proxy for all API calls.
The API Gateway is responsible for routing the request to the right API and handling the response.
It is managing cross-cutting concerns like authentication, authorization, rate limiting, caching, logging, error handling, etc.
Most importantly, the API Gateway sits "in front" of your APIs.
What is the reverse API Gateway pattern?
In contrast to the classic API Gateway pattern,
the reverse API Gateway is a pattern where the API Gateway sits "behind" you application.
Instead of your application making API calls directly to the origin APIs, it makes API calls to the API Gateway, which then forwards the request to the origin API.
This pattern comes with a couple of benefits like we've mentioned above.
The most important one is that we get visibility into all API calls our application is making.
Traditionally, this would be very hard to achieve, especially with SDKs.
With the universal API ORM, you get this for free,
and you don't even notice that you're using a reverse API Gateway.
How does the universal API ORM work?
First, we need to add two API dependencies to your WunderGraph application.
You can do so by using the introspect
API from the WunderGraph SDK.
// wundergraph/wundergraph.config.ts
const weather = introspect.graphql({
id: 'weather',
apiNamespace: 'weather',
url: 'https://weather-api.wundergraph.com/',
});
const countries = introspect.graphql({
apiNamespace: 'countries',
url: 'https://countries.trevorblades.com/',
});
configureWunderGraphApplication({
apis: [weather, countries],
experimental: {
orm: true,
},
});
The introspect
API will fetch the GraphQL schemas from the APIs and generates the ORM code.
We could use the same API to introspect REST or SOAP APIs, or even databases like SQLite, MySQL, PostgreSQL, etc. but for this example we keep it simple.
As the ORM is currently experimental, we need to enable it in the configuration.
We believe that you should ship as early as possible
Next, let's define an API Endpoint, so we can use the ORM and expose some data to our users.
// wundergraph/operations/weather.ts
import { createOperation } from "../../generated/wundergraph.factory";
export default createOperation.query({
handler: async ({ graph }) => {
const germany = await graph
.from('countries')
.query('country')
.where({ code: 'DE' })
.exec();
const weather = await graph
.from('weather')
.query('getCityByName')
.where({ name: germany.capital || '' })
.select(
'weather.summary.icon',
'weather.summary.description',
'weather.summary.title',
).exec();
return {
germany,
weather,
}
}
});
We can now use curl to try out our API Endpoint.
curl http://localhost:9991/operations/weather
At this point, the "What" should be clear,
let's talk about the "How".
When you use the introspect
API, WunderGraph will do a couple of things for you.
It will introspect all sorts of APIs and generate a unified GraphQL Schema across all of them.
We call this the "Virtual Graph", because it's a virtual representation of all APIs you're using.
As multiple APIs might have types with the same name, we prefix all types with the API namespace.
Next, we generate a TypeScript ORM on top of this Virtual Graph.
This ORM is aware of the Virtual Graph and namespaces,
and we're also generating TypeScript types to make from
, query
, where
, select
and exec
type safe.
When you call exec
we're internally generating a GraphQL Operation and send it to the internal GraphQL Endpoint of our API Gateway.
The API Gateway will parse, plan and optimize the Operation against the Virtual Graph, which will then be cached, so it can be re-used efficiently for subsequent requests.
As we're using the reverse API Gateway pattern for all ORM requests, we're not just able to optimize all sorts of things behind the scenes, like caching and batching,
but we're also able to generate complete traces using OpenTelemetry for all API calls your application is making.
E.g. if you're creating an API Endpoint that calls two APIs, where one depends on the other, you'll be able to see the latency of the whole request and a breakdown of the latency of each API call.
Conclusion and Outlook
As you can see, the universal API ORM together with the reverse API Gateway pattern is a powerful combination to integrate APIs into your application.
As we've discussed, this approach comes with many benefits compared to the traditional approach of using SDKs.
That said, this is just a stepping stone towards our vision of taking API Dependency Management to the next level.
We're now working on the WunderGraph Hub, a central place where you can publish, discover and manage API dependencies and have a dialog between API providers and consumers.
We envision a future where you can see at first glance which APIs your application depends on, manage API keys in a single place, and have a place to discuss API changes, deprecations, and new features.
The universal API ORM brings us one step closer to this vision, as it solves the "consumer" side of the problem.
If you're equally excited about the future for APIs, you can follow me on Twitter or join our Discord Community to stay up to date or get in touch.
Posted on June 5, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.