Why Uniform GraphQL

The motivation and philosophy behind Uniform GraphQL


tl;dr: Type-safety is concerned with compile time whereas GraphQL schemas are concerned with runtime. What we need is a uniform approach that is type-safe at compile time, while preserving a runtime type information that carries smoothly to GraphQL schemas. And that's what uniform-graphql is all about: Helping you build robust GraphQL schemas - and resolvers - by delegating all forms of type-safety to the compiler.

Motivation

GraphQL apis usually fall under two schools of thought: schema-first vs code-first. Schema-first GraphQL apis create the typedefs first - including all queries, mutations and subscriptions - and implement the corresponding resolvers after. Code-first apis on the other hand implement the resolvers first and have the typedefs derived from the code. Both approaches have their pros and cons. Uniform GraphQL falls somewhere in the middle, but it's closer to the code-first camp.

The biggest issue with the currently available code-first approaches emerges during the schema-generation phase: a non-trivial mismatch between the implemented resolvers and the generated schema. Developers can't simply rely on the compile-time type system to make sure that their code will match the generated schema, so they have to resort to other means such as decorators and other runtime checks. But it doesn't have to be that way. As it turns out, this is a perfect job for the type system of TypeScript.

End-to-End Type Safety

Uniform GraphQL is built with compile time type-safety front and center, making it very hard for you to experience type errors at runtime. You will find a simple, streamlined approach that will guide you end-to-end while building your GraphQL api. Common examples include:

  • Creating primitive GraphQL types
  • Composing said GraphQL types to create more complex types
  • Implementing query & mutation resolvers that work on said types
  • Implementing field resolvers on object types

Non-Null First

In GraphQL, types are null-first, which means they are nullable unless explicitly wrapped with a GraphQLNonNull type. In TypeScript on the other hand, types are non-null-first: non-nullable by default unless they are explicitly made nullable. This tension is something that few code-first approaches acknowledge and solve for, which results in schema-code mismatches and general developer pain.

/**
 * In GraphQL, types are nullable unless explicitly made non-nullable.
 */

import {
  GraphQLID,
  GraphQLNonNull,
  GraphQLObjectType,
  GraphQLString,
} from 'graphql';

const User = new GraphQLObjectType({
  name: 'User',
  fields: {
    id: { type: new GraphQLNonNull(GraphQLID) },
    fullName: { type: GraphQLString },
  },
});

When it comes to nullability, Uniform GraphQL sides with TypeScript. We are a code-first library and we want to play nicely with our programming language. This is why everything is non-nullable unless they are explicitly made nullable.

/**
 * In Uniform GraphQL, types are nullable unless
 * explicitly made non-nullable.
 */

import { t } from '@whatsgood/uniform-graphql';

const User = t.object({
  name: 'User',
  fields: {
    id: t.id,
    fullName: t.string.nullable,
  },
});

After all is said and done, we still need to create a GraphQL schema. Below is the runtime typedef of Uniform GraphQL. The key point is that both Uniform GraphQL and the reference graphql library eventually yield the same typedef. The difference however, is that Uniform GraphQL will also keep your actual resolver implementations compile-time type safe.

# Both codes result with the same typedef:

type User {
  id: ID!
  fullName: String
}

Composability

One of GraphQL's main benefits is the reusability and composability of types. You can create an enum type, which you use in an object type, which you use in a list type, which you use in an interface type, which you use in another object type, which you use in a union type and so on.

In Uniform GraphQL, you are able to infinitely compose and reuse your types. This includes self referential and mutually recursive types, while always adhering to our core principle of end-to-end type safety. Keep on reading to see how we handle such use cases.

# Self referential type
type User {
  id: ID!
  friends: [User]! # type A refers to itself
}

# Mutually recursive types

type Person {
  id: ID!
  pets: [Animal]! # type A refers to type B
}

type Animal {
  id: ID!
  owner: Person! # type B refers to type A
}

Uniform Type System

No need to maintain two separate type systems for GraphQL and TypeScript while trying to keep them in sync. Once you create your uniform types, all will be taken care of. You will never need to manually type out function parameter types or return types. Everything is inferred from your uniform types; all you need to do is to fill in the blanks.

Code autocompletion for resolvers

Example 1: The compiler is complaining because the resolve function is incorrectly implemented. When we ask for hints on the membership field, we are shown that we need to return one of the listed type literals.

Args types in TypeScript

Example 2: When we hover over args.id, we see that it's a union type between string and number. All this type information comes directly through the library. While developing our GraphQL api, we don't need to manually write any TypeScript types for our resolvers. This inclues the resolver function arguments and the return type.

Edit on GitHub