iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Object Types

Object types are the bread and butter of a GraphQL schema. Each one declares a set of named, typed fields. Fields can return scalars, other object types, or lists — arbitrarily nested. Resolvers attach behavior.

Schema, resolvers, lists, nullability

EXAMPLE
// 1) Define object types in SDL
const typeDefs = `#graphql
type Query {
    me: User
    user(id: ID!): User
    posts(authorId: ID, first: Int = 20): [Post!]!
}

type User {
    id:        ID!
    email:     String!         # required
    name:      String          # optional
    role:      Role!
    createdAt: DateTime!
    posts:     [Post!]!         # required list of required posts
    avatarUrl: String
}

type Post {
    id:      ID!
    title:   String!
    body:    String!
    author:  User!              # author is always present
    tags:    [String!]!
    publishedAt: DateTime
    comments(first: Int = 50): [Comment!]!
}

type Comment {
    id:     ID!
    body:   String!
    author: User!
    post:   Post!
}

enum Role { GUEST USER EDITOR ADMIN }

scalar DateTime
`;

// 2) Nullability — read carefully
//   String        — nullable string
//   String!       — non-null string (the field MUST return a value)
//   [String]      — nullable list of nullable strings
//   [String!]     — nullable list of non-null strings
//   [String!]!    — non-null list of non-null strings  (most common shape for lists)
//
// Rule of thumb: lists are usually [T!]! — neither null itself nor containing nulls.

// 3) Resolvers — one per field, optional if shape matches
const resolvers = {
    Query: {
        me: (_p, _a, ctx) => ctx.user || null,
        user: (_p, { id }, ctx) => ctx.dataSources.users.byId(id),
        posts: (_p, { authorId, first }, ctx) =>
            ctx.dataSources.posts.list({ authorId, first }),
    },
    User: {
        // Default resolver returns user[fieldName]; override for derived fields.
        posts: (user, _a, ctx) => ctx.dataSources.posts.byAuthor(user.id),
        avatarUrl: (user) =>
            user.avatarKey ? `https://cdn.example.com/${user.avatarKey}` : null,
    },
    Post: {
        author: (post, _a, ctx) => ctx.dataSources.users.byId(post.authorId),
        comments: (post, { first }, ctx) =>
            ctx.dataSources.comments.byPost(post.id, first),
    },
    Comment: {
        author: (c, _a, ctx) => ctx.dataSources.users.byId(c.authorId),
        post:   (c, _a, ctx) => ctx.dataSources.posts.byId(c.postId),
    },
};

// 4) Querying
`
query ProfilePage($id: ID!) {
    user(id: $id) {
        id
        name
        role
        posts {
            id
            title
            publishedAt
            comments(first: 3) {
                id
                body
                author { name }
            }
        }
    }
}
`;

// 5) Avoiding N+1 — DataLoader batches per request
import DataLoader from 'dataloader';

function createUserLoader(db) {
    return new DataLoader(async (ids) => {
        const rows = await db.user.findMany({ where: { id: { in: ids } } });
        const byId = new Map(rows.map((r) => [r.id, r]));
        return ids.map((id) => byId.get(id) || null);   // order MUST match input
    });
}

// Context — created per request
async function context({ req }) {
    return {
        user: await authenticate(req),
        loaders: { user: createUserLoader(db) },
    };
}

// Use in resolvers
const resolvers2 = {
    Post: {
        author: (post, _a, ctx) => ctx.loaders.user.load(post.authorId),
    },
};

// 6) Interfaces — shared fields across object types
const typeDefs2 = `#graphql
interface Node { id: ID! }

type User implements Node { id: ID! email: String! }
type Post implements Node { id: ID! title: String! }

type Query { node(id: ID!): Node }
`;

const resolvers3 = {
    Node: {
        __resolveType(obj) {
            if (obj.email) return 'User';
            if (obj.title) return 'Post';
            return null;
        },
    },
};

// 7) Unions — mutually exclusive object types
const typeDefs3 = `#graphql
union SearchResult = User | Post | Comment

type Query { search(q: String!): [SearchResult!]! }
`;

// 8) Field arguments + default values
type Query {
    posts(first: Int = 20, after: String, status: PostStatus = PUBLISHED): [Post!]!
}

// 9) Computed fields
const resolvers4 = {
    User: {
        displayName: (u) => u.name || u.email.split('@')[0],
        isAdmin:     (u) => u.role === 'ADMIN',
    },
};

// 10) Errors and partial responses
// A non-null resolver that throws nulls out the parent until a nullable ancestor.
// To return partial data without crashing the query, mark error-prone fields nullable.

// 11) Common bugs
//   • Returning Date object for a DateTime scalar without a custom scalar resolver
//   • [Post!]! resolver returns null on error → whole field collapses
//   • Forgetting __resolveType on interfaces/unions → 'cannot determine type'
//   • N+1 queries — Post.author hits DB per post; always use DataLoader
//   • Args named differently in SDL vs resolver — resolver param is destructured from args
//   • Treating type defs as JS strings without the GraphQL highlight comment — lose tooling

Why it matters

Design types around the data shape your clients want, not your database’s. Make lists [T!]!, gate behavior at the field level rather than smuggling auth into a wrapper, and put a DataLoader in context to keep child-field resolvers from turning every query into a fan-out.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
type Post {
    id: ID!
    title: String!
    author: User!
}
Try it Yourself »

Discussion

Loading…