Objects
This will walk you through creating your first object types, some concepts in this guide will be explained further in later guides.
Defining an Object type
When adding a new type to your schema, you'll need to figure how the data behind this type will be represented. Pothos entirely decouples your data from your GraphQL schema, and has many different ways to implement Objects in your schema.
In this guide, we will be implementing a Giraffe
object type:
interface Giraffe {
name: string;
birthday: Date;
heightInMeters: number;
}
The easiest way to create a new Object based in an existing Typescript type is wit the objectRef
method:
const builder = new SchemaBuilder({});
const GiraffeRef = builder.objectRef<Giraffe>('Giraffe');
This will create a new ObjectRef
that can be used to reference the Giraffe
type in other parts
of the schema. By passing in the Giraffe interface, We give the ObjectRef
the information it needs
to ensure that fields we add to the Giraffe type are type-safe, and that any fields that reference
the Giraffe Field return the expected data.
Next, We can need to add an implementation for the Giraffe
type:
const GiraffeRef = builder.objectRef<Giraffe>('Giraffe');
GiraffeRef.implement({
description: 'Long necks, cool patterns, taller than you.',
fields: (t) => ({}),
});
In the implementation, we can add a description (optional) and a function to define the fields available to query on the Giraffe type.
Add some fields
The fields
function receives a FieldBuilder
instance that can be used to define the fields for
your type. the FieldBuilder
will be covered in more details in the fields guide.
GiraffeRef.implement(Giraffe, {
fields: (t) => ({
name: t.exposeString('name'),
height: t.exposeFloat('heightInMeters'),
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);
},
}),
}),
});
You'll notice that we haven't added any additional typescript definitions when defining our fields.
Pothos will uses the type provided to objectRef
to ensure that the fields we add to the Giraffe
type are type-safe. This type is only used to ensure that the implementation is type-safe, but
Pothos we never automatically expose properties from the underlying data without an explicit field
definition.
In the example above, we have examples of "exposing" data from the underlying type, ad well as field that requires some additional logic to resolve.
Add a query
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: GiraffeRef,
resolve: () => ({
name: 'James',
birthday: new Date(Date.UTC(2012, 11, 12)),
heightInMeters: 5.2,
}),
}),
}),
});
We can use the ObjectRef
created earlier as the type
option when defining fields that return the
Giraffe type.
Create a server
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();
Query your data
- Run your server (either with
ts-node
) by compiling your code and running it with node. - Open http://0.0.0.0:3000/graphql to open the playground and query your API:
query {
giraffe {
name
age
height
}
}
Different ways to define Object types
There are many 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.
Using Refs
ObjectRefs (the method shown above) is the most flexible solution, and makes it easy to integrate pothos with data sources that have their own Typescript types.
Object refs can be created using builder.objectRef
, and then implemented by calling the
implement
method on the ref, or by passing the ref to builder.objectType
:
const GiraffeRef = builder.objectRef<GiraffeType>('Giraffe').implement({
description: 'Long necks, cool patterns, taller than you.',
fields: (t) => ({}),
});
When using objectRefs with circular dependencies, ensure that the implement
method is called as
a separate statement, or typescript may complain about circular references:
Using classes
If your data is already represented as a class, Pothos supports using the classes themselves as ObjectRefs. This allows you to define a type-safe schema with minimal typescript definitions.
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;
}
}
builder.objectType(Giraffe, {
// Name is required when using a class as an ObjectRef
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),
}),
}));
Using SchemaTypes
You can also provide type mappings when you create the SchemaBuilder, which allows you to reference the types by name throughout your schema (as a string).
const builder = new SchemaBuilder<{ Objects: { Giraffe: GiraffeType } }>({});
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)),
heightInMeters: 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.