Build JSON APIs with JSON-Schema by writing GraphQL Operations against any DataSource like REST, GraphQL, Apollo Federation, PostgreSQL and MySQL
Jens Neuse, CEO & Founder of WunderGraph
GraphQL is known as a Query Language to Query Graphs. Today, I'd like to show you an unusual use case for the Query language: Building JSON APIs with JSON-Schema validation.
Implementing GraphQL servers and using GraphQL clients to query them is becoming boring, isn't it? How about trying something new? How about doing a crossover between OpenAPI, JSON-Schema and GraphQL?
Sounds interesting? I hope so. Let's do it!
As some might know, I'm the biggest fan of Persisted GraphQL Operations. I don't believe that GraphQL servers should directly expose their API. I keep writing about this topic, for example I think that GraphQL is not meant to be exposed over the internet. Additionally, Persisted Operations help a lot with Versioning and keeping APIs backwards compatible. Another time, I was talking about what happens if we treat the GraphQL Operation as the API schema. In this post, I've listed 13 GraphQL security vulnerabilities and how Persisted Operations help solving them.
Finally, I've wrote extensively about the Fusion of GraphQL, REST, JSON-Schema and HTTP2. The problem is, I've realised that this blog post was a bit too long.
So here I am, writing a shorter one with a bit more focus. Sorry for the backlinking intermission, seo hacks...
Alright, a few facts about persisted GraphQL Operations.
- they are like stored procedures or prepared statements
- you can still supply variables, just not change the content
- most applications don't change the Operations at runtime
So, a persisted GraphQL Operation is a function that optionally takes a few arguments and returns a JSON Object.
Let's have a look at a GraphQL Operation to make things clearer. It's one of my favorite examples because it contains so many cool features.
#CreatePost.graphqlmutation ($name: String! @fromClaim(name: NAME)$email: String! @fromClaim(name: EMAIL)$message: String! @jsonSchema(pattern: "^[a-zA-Z 0-9]+$")){createOnepost(data: {message: $message user: {connectOrCreate: {where: {email: $email} create: {email: $email name: $name}}}}){idmessageuser {idname}}}
This post is not about security or authorization and also not about how we support OpenID Connect, claims injection, etc... If you're interested in this kind of stuff, here are the docs on the @fromClaim directive.
In this post, we'll ignore the magic of the @fromClaim
directive.
If we store this Operation on the server, we can turn it into the following function.
interface CreatePostInput {message: string;name: string;email: string;}interface CreatePostResponse {id: string;message: string;user: {id: string;name: string;};}const CreatePost = (input: CreatePostInput) :CreatePostResponse => {// database access codereturn {...}}
As you can see, it's a function that takes an input and returns a response. Both, input and response can be expressed as a JSON Object.
For the response, it's obvious. GraphQL returns JSON. Let me explain the variables part.
GraphQL Variables can either be supplied "inline", meaning, as part of the GraphQL Operation / Document. If we're de-inlining all GraphQL Variables, they "must" be supplied as a JSON Object.
WunderGraph does this de-inlining by default which brings us to an important conclusion.
A Persisted GraphQL Operation is a function that takes a JSON Object and returns a JSON Object.
A function with this specification is actually very useful!
If we walk through the AST of this GraphQL Operation, we're able to extract the exact JSON structure of both input and response. The graphql-js implementation has an excellent visitor implementation which makes this quite easy.
If we know the JSON structure, we can even go one step further. We can create a JSON-Schema for the input and the response!
That's what we do automatically, so let's have a look at the generated JSON-Schemas!
First, the input Schema.
{"type": "object","properties": {"message": {"type": "string","pattern": "^[a-zA-Z 0-9]+$"}},"additionalProperties": false,"required": ["message"]}
The input for our Operation is a JSON object with exactly one property, the message.
Notice how the fields name
and email
are not present in the input.
That's because the user is not allowed to enter them, we're injecting them using the users' OpenID Connect Claims, but that's another topic.
What's important is that the Regex pattern got applied to the message,
additional properties are not allowed, and the message is required, that's because the Variable definition is String!
,
the bang stands for mandatory.
Second, the response schema:
{"type": "object","properties": {"data": {"type": "object","properties": {"createOnepost": {"type": "object","properties": {"id": { "type": "integer" },"message": { "type": "string" },"user": {"type": "object","properties": {"id": { "type": "integer" },"name": { "type": "string" },"email": { "type": "string" } },"additionalProperties": false,"required": ["id", "name", "email"]}},"additionalProperties": false,"required": ["id", "message", "user"]}},"additionalProperties": false}},"additionalProperties": false}
The response schema is less cool. GraphQL Operations return JSON, so we can map all fields of an Operation to a JSON-Schema by walking through the Operation AST.
With these JSON-Schema definitions, we're now able to run Code-Generators for any language, TypeScript, Java, Go, Rust, etc... and generate TypeSafe clients.
Benefits of using JSON API with JSON-Schema over GraphQL#
We've learned an important lesson from OpenAPI Specification, JSON-Schema is powerful! JSON-Schema is THE standard to define a schema for JSON, it's well maintained and there are a lot of beneficial integration points.
Using JSON Schema for Server-Side Input Validation#
JSON-Schema can be used to validate inputs on the server-side. There are implementations in many languages that do this, WunderGraph is using a Go implementation as our Engine is written in Go.
Using JSON Schema to build Forms#
There's also tools like React JSON Schema Form that allow you to generate React forms from a JSON Schema. We've built a tight integration into WunderGraph with this capability. This allows you to build forms just by writing a GraphQL Operation, everything from backend, validation on both client and server, can be generated. This approach is very developer friendly and saves you a lot of time. All you have to do is connect your backend and tweak the UI.
JSON API in front of GraphQL makes it compatible to the web and avoids vendor lock-in#
Another benefit of using JSON API over GraphQL is that each Operation gets its own unique URL. Combined with the fact that Queries use the HTTP GET method and Mutations use HTTP POST, we're making GraphQL compatible to the web again.
By compatible, I mean you can leverage the existing infrastructure of the web, like browsers, Caches, CDNs, Proxies, etc...
Sending READ requests over HTTP POST breaks most of the infrastructure of the web. If instead, we're using GET for READS and POST for WRITES, we're good.
You can put Varnish, NginX or Cloudflare in front of a JSON API and it just works, no extra configuration required, no vendor lock-in into a proprietary Caching Engine.
JSON API can be understood by Postman#
JSON APIs are not exactly RESTful. They implement the most important constraints of REST which make them compatible with the web, e.g. a unique URL per Resource, but are still not fully RESTful. That's totally fine because the goal is never to be RESTful to create a good API Developer experience.
That said, even without being fully RESTful, it's close enough to be used in similar ways.
That's why we've added a small change to our Code-Generator with a big impact. Each WunderGraph application automatically generates a Postman Collection!
This means, you can easily try it out, play with it and share your JSON API with your colleagues or even another party.
JSON API in front of GraphQL increases security#
I wrote in another blog post about the 13 most common vulnerabilities in GraphQL APIs. If we put a JSON API in front of our "virtual" GraphQL API, we eliminate all problems correlated to the Query Language. By adding JSON-Schema validation on top, we're adding an extra layer of security for input validation.
Conclusion#
As we've discussed, adding a JSON API with JSON-Schema in front of your GraphQL API has a lot of benefits, but there's more to it.
I don't see GraphQL as a simple Query Language where you build a server, add a client and call it a day.
The approach described above is the fastest way to implement a Backend For Frontend (BFF) on the fly.
Combine it with the capability to add any number of DataSources and upstream protocols, and we're turning GraphQL into way more than just a Query Language.
GraphQL as an ORM to your APIs, an API Orchestration Layer.
Currently, you can use REST, GraphQL, Apollo Federation (with Subscriptions), PostgreSQL and MySQL as a DataSource for your WunderGraph application. WunderGraph merges all DataSources into one unified "virtual Graph" which you can then securely expose as a JSON API by writing GraphQL Operations.
This is all you need to build a BFF.
In comparison to e.g. just using generated GraphQL APIs, we're adding an abstraction layer, the JSON API. This allows us to decouple API consumers from the DataSources itself.
In the future, you'll be able to easily swap out DataSources, e.g. change PostgreSQL with MySQL, switch from a REST to a gRPC API, etc..., all without breaking the JSON API contract.
Learn more about other features we provide and give it a try on your local machine!
yarn global add @wundergraph/wunderctlmkdir wundergraph-democd wundergraph-demowunderctl init
If you're looking for more advanced examples, have a look at this one using Apollo Federation and REST APIs or this example with a Realtime Chat application on top of PostgreSQL.
What to read next
This is a curated list of articles that I think you'll find interesting.
- In the WunderHub Announcement, I talk about how WunderHub will change the way we share and collaborate on APIs. It allows you to share APIs like npm packages.
- How automating API integrations benefits your business is dedicated to C-level executives who want to learn more about the business benefits of automating API integrations.
- Another interesting topic is to JOIN APIs without Schema Stitching or Federation, just by using a single GraphQL Operation
- For those interested in the most common GraphQL Security vulnerabilities, I suggest to read about them and how WunderGraph helps you to avoid them.
- A classic post but still relevant is I believe that GraphQL is not meant to be exposed over the Internet. It's a controversial topic and many misunderstand it. But think about it, why is HTTP not mentioned a single time in the GraphQL specification?
- One very common problem of using GraphQL is the Double Declaration Problem, the problem of declaring your types over and over again. This post explains that it's even more complicated than just double declaration and how we can solve it.
- The Fusion of GraphQL REST and HTTP/2 is a very long post, probably too long for a blog post. But if you're interested in a deep dive on the motivations behind creating WunderGraph, this is the post for you.
About the Author
Jens Neuse, CEO & Founder of WunderGraph
Jens has experience in building native apps for iOS and Android, built hybrid apps with Xamarin, React Native and Flutter, worked on backends using PHP, Java and Go. He's been in roles ranging from development to architecture and led smaller and larger engineering teams.
Throughout his whole career he realized that working with APIs is way too complicated, repetitive and needs a lot more standardization and automation. That's why he started WunderGraph, to make usage of APIs and collaboration through APIs easier.
He believes that businesses of the future will be built on top of collaborative systems that are connected through APIs. Making usage, exploration, sharing and collaboration with and through APIs easier is key to achieve this goal.
Follow and connect with Jens to exchange ideas or simply participate in his feed of thoughts.