In this post, we will compare how two schema builders, Pothos and TypeGraphQL, can aid developers in building GraphQL schemas for their services — and how to actually build schemas using TypeScript with these tools — through the following sections:
At the end of the post, we’ll compare the features offered by both tools and some use cases.
GraphQL schemas consists of object types with fields that specify the types of data they can accept. These types are known as scalar types and they are built into the SDL core. Once we define a GraphQL schema, only the specified fields defined in the objects with their respective types can be queried or mutated.
Within a GraphQL schema, we must define our query and mutation types at the point of creating the schema, except for mutation types, which are not always compulsory. Both type definitions define the entry point of every query we make to a GraphQL service or API, based on the predefined schema. You can read more about schema building in GraphQL elsewhere on the blog.
Pothos is plugin that offers an easy way to create and build schemas with the GraphQL and TypeScript.
Since it is TS-based, Pothos offers the type safety necessary for GraphQL schema building. It also builds upon TypeScript’s powerful type system and type inferences, requiring no need for code generation or using manual types everywhere.
Pothos schemas build up into a plain schema that uses types from the graphql
package. This means it should be compatible with most of the popular GraphQL server implementations for Node.js. In this guide, we will use @graphql-yoga/node
to run our examples, but you can use whatever server you want.
In Pothos, you usually begin with the shape of your data (defined as a type, interface, class, Prisma model, etc.) and then define a GraphQL type that uses that data, but doesn’t necessarily conform to its shape. The approach Pothos takes feels more natural in larger applications, where you have real data that isn’t purely created for your GraphQL API.
Apart from the advantage of its first-hand type safety — which is independent of decorators — Pothos prides itself on providing lots of features exposed as plugins, which comprise a large ecosystem of plugins. One of Pothos’s major advantages is the separation of the GraphQL API and how data is represented internally in the schema, which we are going to see as we proceed.
Let‘s start with an example from the Pothos documentation: building a simple schema from a “Hello, World!” app.
import { createServer } from '@graphql-yoga/node'; import SchemaBuilder from '@pothos/core'; const builder = new SchemaBuilder({}); builder.queryType({ fields: (t) => ({ hello: t.string({ args: { name: t.arg.string(), }, resolve: (parent, { name }) => `hello, ${name || 'World'}`, }), }), }); const server = createServer({ schema: builder.toSchema({}), }); server.start();
In this example, we create a simple boilerplate server with graphl-yoga/node
and the Pothos schema builder. We import the schema builder from Pothos core and instantiate a new schema builder, which constructs a plain schema that the GraphQL language understands.
After that, we setup the query builder with our field types and arguments in a type safe way. The resolver is responsible for returning a response when the query executes after all necessary validation has been done on the field arguments passed to the query and on the query itself.
Finally, we pass the built schema into the createServer
function and call the server.start
method.
With Pothos, we can define the structure of our data in form of object types, which lets us know the details of the underlying data types. After that, we can then go ahead to define the types, where we pass the structure that we have defined as a way of validating the actual types.
So, basically, we need a way of passing type information about our underlying data structure so that our fields know what properties are available on the object type.
With the help of type inferences, we can confirm when we pass the wrong fields on an object type, and be sure the objectType
conforms with our type definitions, since the object can tell what types to expect. Based on the fields defined in the schema, we can then determine the nature of the available data and their types. This means that any data we ever intend to add our schema has to be explicitly defined.
There are three ways of defining objects in Pothos: using classes, schema types, and refs.
Defining Pothos classes is the same as defining regular classes — we structure our data and initialize the class. See an example below:
export class Person { name: string; age: number; constructor(name: string, age: number) { this.name = name; this.age = age; } }
After defining the class, we can map the types for the fields in the above class. This is done using the Pothos field builder to validate against the object types in our schema class above. See how we can do that below.
<
pre class=”language-graphql hljs>const builder = new SchemaBuilder({});
builder.objectType(Person, {
name: ‘Person Schema’,
description: “A person schema”,
fields: (t) => ({}),
});
The objectParam
argument, Person
, represents our initial class, which serves as a blueprint for validating against the kind of types that we can pass for each of the individual properties based on that blueprint. We do this so that, when we use these properties in the fields, we can be sure they represent the correct type.
We can proceed to define the kinds of data we have in our schema above with the help of the field object above. Let us see how we do that below:
builder.objectType(Person, { name: 'Person Schema', description: 'A person schema', fields: (t) => ({ nameNew: t.string({ resolve: (parent) => { const name = console.log(parent.name) }, }), ageNew: t.int({ resolve: (parent) => { const age = console.log(parent.age) }, }), }), });
As we can see, we are unable to directly access the properties defined in our schema. This is by design, so as to ensure we only get access to the properties from the underlying schema.
Note that the parent arg will be a value of the backing model for the current type specified in the schema class.
However, to get direct access to the field properties defined in the schema or model, we can make use of expose
, as defined here in the docs.
exposeString(name, options)
Note: The name
arg can be any field from the backing model that matches the type being exposed.
Next, we actually write a query that resolves to the actual values with the help of a resolver, which is a function that resolves the value of this field. Let’s create such a query with Pothos below.
builder.queryType({ fields: (t) => ({ Person: t.field({ type: Person, resolve: () => new Person('Alexander', 25), }), }), });
The next step is to create a simple server and pass the schema we built to the server, as we have seen earlier. Pothos works well with any popular GraphQL servers implementations available.
Lastly, we run our regular GraphQL queries against the server.
query { Person { name age } }
As we have outlined above, in creating or defining object types with Pothos (which is a way of providing type information about how the underlying data in your schema is structured), we can either make use of classes as above, schema types or even with refs. More information on how to use this based on our use case can be found in the docs.
With Pothos, we can generate our schema file using graphql-code-generator
. You can also print your schema, which is useful when you want to have an SDL version of your schema. In this scenario, we can use printSchema
or lexicographicSortSchema
, both of which can be imported from the GraphQL package.
Pothos does not have an inbuilt mechanism for generating types to use with a client, but graphql-code-generator can be configured to consume a schema directly from your TypeScript files.
Pothos enforces clear separation between the shape of your external GraphQL API and the internal representation of your data. To help with this separation, Pothos offers a backing model that gives you a lot of control over how you define the types that your schema and resolver use.
Pothos’s backing model is extensively explained in the docs.
Pothos is plugin-based, offering sample plugins like simple-objects
, scope-auth
, and mocks
that make your work easier. For example, the simple-objects
plugin can make building out a graph much quicker because you don’t have to provide explicit types or models for every object in your graph.
Pothos is unopinionated about how code is structured, and provides multiple ways of doing many things. In fact, Pothos goes as far as providing a guide for how to organize your files.
To create a schema with Pothos, all we have to do is import the schemaBuilder
class from Pothos core, as shown below.
import SchemaBuilder from '@pothos/core'; const builder = new SchemaBuilder<{ Context: {}; }>({ // plugins });
The schema builder helps create types for our graph and embeds the created types in a GraphQL schema. Details on the Pothos schema builder API design can be found in the docs.
While Pothos is mainly a schema builder, it also has support for and integrates well with most ORMs, especially Prisma via the Prisma plugin for Pothos. With this plugin, we can easily define Prisma-based object types and, equally, GraphQL types based on Prisma models. An example and setup on how to go about this is shown in the documentation.
Of course, one of the notable features of this integration is the support for strongly typed APIs, automatic query optimization (including the n + 1
query problem for relation), support for many different GraphQL models based on the same database schema, and so on. The documentation covers it all.
Note: Prisma can also be integrated directly with Pothos. The plugin just makes it easier, more performant, and more efficient for us to work with these two technologies. The guide on how to perform this integration contains more information.
TypeGraphQL offers a different approach to building schemas. With TypeGraphQL, we define schemas using only classes and decorator magic. It is mainly dependent on graphql-js
, class-validator()
, and the reflect-metadata
shim, which makes reflection in TypeScript work. class-validator
is a decorator-based property validation for classes.
We extensively covered building GraphQL APIs with TypeGraphQL in an earlier post, including how object types are created:
@ObjectType() class Recipe { @Field() title: string; @Field(type => [Rate]) ratings: Rate[]; @Field({ nullable: true }) averageRating?: number; }
As we can see above, we start defining schemas with TypeGraphQL by defining classes, which serve as a blueprint for our schema. Let’s see an example below.
First, we begin by creating types that resemble the types in the SDL.
type Person { name: String! age: Number dateofBirth: Date }
Next, we can proceed to create the class, which must contain all the properties and defined types for our Person
type.
class Recipe { name: string; age?: number; dateofBirth: ate }
We make use of decorators to design the classes and its properties, like so:
@ObjectType() class Person { @Field() name: string; @Field() age?: number; @Field() dateOfBirth: Date; }
Then, we create what we call input
types, which we need to perform our queries and mutations.
@InputType() class NewPersonInput { @Field() @MaxLength(40) name: string; @Field({ nullable: true }) age?: number; @Field() dateOfBirth: Date; }
Field validation methods, including maxLength
, are from the class-validator
library. After creating regular queries and mutations, the last step is to build the schema that we will pass to our GraphQL server.
const schema = await buildSchema({ resolvers: [PersonResolver], });
An example mutation type for our Person
type is shown below:
type Mutation { addNewPerson (newPersonData: NewPersonInput!): Person! deletePerson(id: ID!): Boolean! }
TypeGraphQL features include validation, authorization, and more, which help developers write GraphQL APIs quickly and reduces the need to create TypeScript interfaces for all arguments and inputs and/or object types. TypeGraphQL also helps ensure that everyone works from a single source of truth by defining the schema using classes and a bit of decorator help. This would indeed help in reducing code redundancy.
TypeGraphQL supports dependency injection by allowing users to provide the IoC container that will be used by the framework.
Field properties are strictly validated with the class validation library. TypeGraphQL is more flexible than Pothos and supports generic types in cases where we might need to declare the types of some fields in a more flexible way, like a type parameter.
TypeGraphQL supports custom decorators, including method and parameter, which offers a great way to reduce boilerplate code and reuse common across multiple resolvers.
TypeGraphQL also has huge support for multiple different third-party ORMs, including TypeORM and Prisma. With Prisma, TypeGraphQL provides an integration with the typegraphql-prisma package, which we can find on npm.
TypeGraphQL already has provisions for generating type classes and resolvers based on your Prisma schema, which means that we do not have to write too much code to perform regular queries and mutations. The documentation has examples of setting these two technologies up and also a dedicated website, which contains more examples and tutorials, including installation instructions, configuration and more.
In this post, we have looked at the approach to schema building for two awesome, TypeScript-based libraries. Although Pothos can be used to build TypeScript-based GraphQL APIs, it shines mainly as a schema builder.
TypeGraphQL, on the other hand, is more flexible and allows us to build simple GraphQL APIs with support for different ORMs.
We have been able to cover some of the important features, use cases and methodologies for schema building in your Node.js/TypeScript and GraphQL-based APIs. The aim of this post is to show you how these two different and unique libraries have approached these processes, so that you can make an informed decision about the next best tools to use in your future projects.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your site. Instead of guessing why problems happen, you can aggregate and report on problematic GraphQL requests to quickly understand the root cause. In addition, you can track Apollo client state and inspect GraphQL queries' key-value pairs.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.