This will walk you through creating your first object types, some concepts in this guide will be explained further in later guides.
When adding a new type to your schema, you'll need to figure how the data behind this type will be represented. In this guide, we will use a class to represent some information about giraffes. Using classes is completely optional, but it's a good place to start, since it makes it easy to show all the different ways that you can tie the shape of your data to a new object type.
export class Giraffe {
name: string;
birthday: Date;
heightInMeters: number;
constructor(name: string, birthday: Date, heightInMeters: number) {
this.name = name;
this.birthday = birthday;
this.heightInMeters = heightInMeters;
}
}
You can use builder.objectType
to add new Object
types to your schema.
const builder = new SchemaBuilder({});
builder.objectType(Giraffe, {
name: 'Giraffe',
description: 'Long necks, cool patterns, taller than you.',
fields: (t) => ({}),
});
The first argument is an ObjectParam
, in this case the class that represents our giraffes. This is
used to convey type information about our underlying data, so that fields can know what properties
are available on the parent object.
Fields define what data is available in your schema
builder.objectType(Giraffe, {
name: 'Giraffe',
description: 'Long necks, cool patterns, taller than you.',
fields: (t) => ({
name: t.exposeString('name', {}),
age: t.int({
resolve: (parent) => {
// Do some date math to get an approximate age from a birthday
const ageDifMs = Date.now() - parent.birthday.getTime();
const ageDate = new Date(ageDifMs); // milliseconds from epoch
return Math.abs(ageDate.getUTCFullYear() - 1970);
},
}),
height: t.float({
resolve: (parent) => parent.heightInMeters,
}),
}),
});
In Pothos we never automatically expose properties from the underlying data. Each property we want
to add in our schema needs to be explicitly defined. The fields
property in the options object should
be a function that accepts one argument (a FieldBuilder) and returns an
object whose keys are the field names, and whose values are FieldRefs
created by the
FieldBuilder. Fields are explained in more detail in the
fields guide.
We can create a root Query
object with a field that returns a giraffe using builder.queryType
builder.queryType({
fields: (t) => ({
giraffe: t.field({
type: Giraffe,
resolve: () => new Giraffe('James', new Date(Date.UTC(2012, 11, 12)), 5.2),
}),
}),
});
The type
parameter can use whatever was used as the first argument of builder.objectType
, in
this case the Giraffe class. builder.objectType
also returns a Ref
object that can be used as a
TypeParam
.
Pothos schemas build 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. In this guide
we will use graphql-yoga
but you can use whatever server you want.
import { createServer } from 'http';
import { createYoga } from 'graphql-yoga';
const yoga = createYoga({
schema: builder.toSchema(),
context: (ctx) => ({
user: { id: Number.parseInt(ctx.request.headers.get('x-user-id') ?? '1', 10) },
}),
});
export const server = createServer(yoga);
server.listen(3000);
// Build schema and start server with the types we wrote above
const server = createServer({
schema: builder.toSchema(),
});
server.start();
ts-node
) by compiling your code and running it with node.query {
giraffe {
name
age
height
}
}
There are 3 different ways that you can provide type information to Pothos about what the underlying data in your graph will be. Depending on how the rest of your application is structured you can pick the approach that works best for you, or use a combination of different styles.
This is the approach used above. If your data is already represented as a class, this is a fairly
straight forward approach, since you can just use your existing classes anywhere that a TypeParam
is expected.
const builder = new SchemaBuilder({});
builder.objectType(Giraffe, {
name: 'Giraffe',
description: 'Long necks, cool patterns, taller than you.',
fields: (t) => ({}),
});
builder.queryFields((t) => ({
giraffe: t.field({
type: Giraffe,
resolve: () => new Giraffe('James', new Date(Date.UTC(2012, 11, 12)), 5.2),
}),
}));
You can provide a type mappings when you create the SchemaBuilder. This will allow you to reference the type by name throughout your schema (as a string).
const builder = new SchemaBuilder<{ Objects: { Giraffe: Giraffe } }>({});
builder.objectType('Giraffe', {
description: 'Long necks, cool patterns, taller than you.',
fields: (t) => ({}),
});
builder.queryFields((t) => ({
giraffe: t.field({
type: 'Giraffe',
resolve: () => new Giraffe('James', new Date(Date.UTC(2012, 11, 12)), 5.2),
}),
}));
This is ideal when you want to list out all the types for your schema in one place, or you have interfaces/types that define your data rather than classes, and means you won't have to import anything when referencing the object type in other parts of the schema.
The type signature for SchemaBuilder is described in more detail
later, for now, it is enough to know that the Objects
type provided to
the schema builder allows you to map the names of object types to type definitions that describe the
data for those types.
You can use an ObjectRef
to reference your class and provide a Generic
argument that describes
the shape or your data.
const builder = new SchemaBuilder({});
const Giraffe = builder.objectRef<GiraffeShape>('Giraffe');
builder.objectType(Giraffe, {
description: 'Long necks, cool patterns, taller than you.',
fields: (t) => ({}),
});
builder.queryFields((t) => ({
giraffe: t.field({
type: Giraffe,
resolve: () => ({
name: 'James',
birthday: new Date(Date.UTC(2012, 11, 12)),
height: 5.2,
}),
}),
}));
ObjectRefs
are useful when you don't want to define all the types in a single place
(SchemaTypes
) and your data is not represented as classes. Regardless of how you define your
object types, builder.objectType
returns an ObjectRef
that can be used as a type parameter in
other parts of the schema.
A slightly simplified version of the above could be written as
const builder = new SchemaBuilder({});
const Giraffe = builder.objectRef<GiraffeShape>('Giraffe').implement({
description: 'Long necks, cool patterns, taller than you.',
fields: (t) => ({}),
});
builder.queryFields((t) => ({
giraffe: t.field({
type: Giraffe,
resolve: () => ({
name: 'James',
birthday: new Date(Date.UTC(2012, 11, 12)),
height: 5.2,
}),
}),
}));
If the type you are defining has a circular reference to itself (either directly, or through another type) you may need to keep `builder.objectRef` and `ref.implement` as separate statements for at least one of the types to allow typescript to correctly resolve the types for your circular references.