Build flexible GraphQL APIs by treating the Schema like a Database
Tags: graphql, database, webdev, programming
Published: 2022-09-13 17:35:46


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mdm02jas5ta58rn7qwjj.png)


---
In this post, I'd like to talk about a new Architecture pattern for building flexible GraphQL APIs. By **treating your GraphQL Schema like a Database**, you're able to build use-case agnostic and flexible GraphQL APIs.

We're currently in the works of building WunderGraph Cloud,
a Serverless GraphQL API Platform with integrated CI/CD, similar to Vercel.

Our goal is to offer the best possible Developer Experience for building APIs, with branching and the ability to deploy multiple versions of the same API just by opening a Pull Request.

You can sign up for [early access if you're interested](/cloud-early-access).

Building WunderGraph Cloud means that we have to build APIs ourselves. We made the decision to use the [Open Source WunderGraph Framework] (https://github.com/wundergraph/wundergraph) to build our own APIs.

We believe that the best way to build a great Developer Experience is to use the same tools that we're building for our customers.

Using WunderGraph to build APIs is fundamentally different from the traditional approach.

Instead of directly exposing the GraphQL Layer, we're hiding it behind a JSON-HTTP/JSON-RPC API. All the plumbing is handled by our Framework.

This protection against direct exposure of the GraphQL layer has a lot of advantages.

But before we get there, let's discuss the traditional approach.

## Building GraphQL APIs: The viewer root field

One common pattern you often see in GraphQL APIs is the `viewer` root field. It's a field that returns the currently authenticated user.

Here's a simple example:

```graphql
type Query {
  viewer: User!
}
type User {
  id: ID!
  name: String!
}
```

If you send a GraphQL query like below, it will return the currently authenticated user:

```graphql
query {
  viewer {
    id
    name
  }
}
```

The implementation of the `viewer` field usually looks like this:

1. A middleware extracts the current user from the request, e.g. from a cookie or a JWT token
2. The middleware injects the user object into the resolver context
3. The resolver of the `viewer` field uses the userID from the context to fetch the user from the database

With this pattern, you can add relationships like groups, friends, etc. to the `User` type, allowing each user to access "their" data.

What sounds great at first glance comes with a lot of drawbacks, actually.

What if we don't have a user?
What if we want to see the data of another user?
What if we don't know exactly how authentication will be ultimately implemented?

## The limits of building GraphQL APIs with a user context in mind

Sometimes, you don't have a user. If you're building a frontend, this might not be obvious, but there are a lot of use cases where we actually don't have one.

Our API could be used by other microservices. They don't operate in the context of a user, but in the context of a service.

They could be authenticated, but they will not have a user ID.

Another example is when we build a CLI tool. The CLI might be used from a CI/CD pipeline.

In this case, we might be injecting service credentials into the environment of the CI runner. Again, no user.

How about building an admin dashboard to help your users?
WunderGraph Cloud will allow users to deploy "projects".

If there's a problem with one of the projects, how could our support team access the data if they need to be authenticated as a user?

As you can see, there are a lot of use cases where we'd have to circumvent the `viewer` root field. Possible workarounds might be to create a second GraphQL API only for admin users.

Another option would be to create fields prefixed with `admin_`.

These special fields would grant you more flexible access and require you to be authenticated as an admin user.

Either way, you'd have to maintain another service or another set of resolvers. It's a costly approach that we'd ideally want to avoid.

## Building Authentication-agnostic GraphQL APIs around the idea of "actors"

As we've stated above, WunderGraph hides your GraphQL API behind a JSON-RPC layer.
This means, you can build your API Authentication-agnostic.

Let's take a look at the first iteration of our API.

```graphql
type Query {
	userByID(id: ID!, actorID: ID!): MaybeUser!
}
type User {
    id: ID!
    email: String!
    firstName: String!
    lastName: String!
    slug: String!
}
union MaybeUser = NotFound | User
type NotFound {
    message: String!
}
```

There's no `viewer` root field. Instead, we have a `userByID` field that takes a second argument, the `actorID`.

You might be thinking that this API is insecure,
because it allows you to query any user by ID. But that's not the case.

Let's see how we can leverage WunderGraph to implement all use cases we've discussed above without compromising security.

### Let's build the first Operation for the user to view their own profile

```graphql
query($currentUserID: ID! @fromClaim(name: USERID)) {
    userByID(id: $currentUserID, actorID: $currentUserID) {
        ... on User {
            id
            email
            firstName
            lastName
            slug
        }
        ... on NotFound {
            message
        }
    }
}
```

This Operation leverages the [`@fromClaim` directive](https://docs.wundergraph.com/docs/directives-reference/from-claim-directive).

This directives injects the userID into both query variables, userID and actorID are the same.

The user must be authenticated to access this field.
They can either be authenticated via a cookie or a JWT token.

The logic to implement the underlying resolver could check if the `actorID` is the same as the `userID`. If that's the case, the user is allowed to access the data.

### Next, let's build the Operation for the admin to view any user's profile

```graphql
query($userID: ID! $actorID: ID! @fromClaim(name: USERID)) {
    userByID(id: $userID, actorID: $actorID) {
        ... on User {
            id
            email
            firstName
            lastName
            slug
        }
        ... on NotFound {
            message
        }
    }
}
```

In this case, we're only injecting the `actorID` from the user context.

The `userID` variable can be defined by the `viewer` of the API.

For this Operation to work, we need to extend the logic of our resolver.

E.g. we can check in the database if the `actorID` (injected) is an admin user. 

### Next, let's build an Operation that can be called from another microservice

Actually, we've solved this problem already.
A microservice can use the OpenID Connect client credentials flow to authenticate.

This means it will acquire a JWT token with a `sub` claim.
The `sub` claim is injected into the `@fromClaim(name: USERID)` directive. This means, the Operation above can be called from a microservice.

This can be implemented in the resolver by checking the `actorID` for a specific prefix, e.g. `svc_`. If the `actorID` starts with `svc_`, we know that the request is coming from a microservice.

We can then check in our database whether the service is allowed to access the data.

### Finally, let's build an Operation that can be called from a CI/CD pipeline

Again, this is the same as the previous use case. The only difference might be the issuing of an access token that grants access to all users of an organization.

This means the `actorID` would be something like `org_1234`.

## Treating our API like a database makes it flexible and easy to maintain

As you've seen, we've been able to implement all use cases with a single resolver. We didn't need to create a second API or a second set of resolvers. All of this is possible because we're hiding the GraphQL layer behind the WunderGraph API Gateway.

Due to the fact that we know we're going to hide the GraphQL layer, we can design it differently. That's why the title states that we're treating our API like a database.

A database usually doesn't care about the user context.
This makes it very flexible but also vulnerable.
However, we're (hopefully) hiding the database behind an API layer, so it's not an issue. With WunderGraph, we're applying the same principle to our GraphQL API.

The flexibility is great! We're able to write and maintain less code. But there's another benefit in terms of security:
We're able to easily audit access to our data.

## Building GraphQL APIs with `actors` makes data access easy to audit

When every API call needs to have an actor, we know exactly who had access to which data, and when.

If an access token is compromised, We know exactly which Operations were called with that token (actorID) in question
The WunderGraph API Gateway can produce an audit log for us.

## Conclusion

We've shown a pattern to build flexible and secure GraphQL APIs with the additional benefit of easy auditing.

I hope this inspires you to think in new ways about how to build your GraphQL APIs.

If you're interested in learning more about how we're building WunderGraph and how you can use it,
please follow us on twitter, linkedin, or [join our discord](https://wundergraph.com/discord) to get updates.