Skip to Content

Build JSON APIs with JSON-Schema by writing GraphQL Operations against any DataSource like REST, GraphQL, Apollo Federation, PostgreSQL and MySQL

Published: October 19, 2021
Jens Neuse

Jens Neuse

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.graphql
mutation (
$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}}}}){
id
message
user {
id
name
}
}
}

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 code
return {
...
}
}

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/wunderctl
mkdir wundergraph-demo
cd wundergraph-demo
wunderctl 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.

About the Author
Jens Neuse

Jens Neuse

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.

Comments

Product

Subscribe to our newsletter!

Stay informed when great things happen! Get the latest news about APIs, GraphQL and more straight into your mailbox.

© 2021 WunderGraph