Scalar Types
GraphQL is strongly typed. The schema declares object types, scalars, enums, interfaces, unions, input types. Clients and servers both validate against it — no docs drift.
Scalars, objects, enums, interfaces, unions, inputs
EXAMPLE
# 1) Built-in scalars
# Int, Float, String, Boolean, ID
# 2) Object types — the bulk of a schema
type User {
id: ID! # ! = non-null
email: String!
name: String
age: Int
posts: [Post!]! # array of non-null Posts, array itself non-null
createdAt: DateTime!
}
type Post {
id: ID!
title: String!
body: String!
author: User!
tags: [String!]!
}
# 3) Custom scalars — for DateTime, JSON, URL, etc.
scalar DateTime
scalar JSON
scalar URL
# Apollo Server — wire up a custom scalar resolver
import { GraphQLScalarType, Kind } from 'graphql';
const DateTimeScalar = new GraphQLScalarType({
name: 'DateTime',
description: 'ISO 8601 date-time',
serialize(value) { return value instanceof Date ? value.toISOString() : value; },
parseValue(value) { return new Date(value); },
parseLiteral(ast) { return ast.kind === Kind.STRING ? new Date(ast.value) : null; },
});
# 4) Enums
enum Role { ADMIN, EDITOR, USER, GUEST }
enum OrderStatus { PENDING, PAID, SHIPPED, CANCELLED }
type User {
role: Role!
}
# 5) Interfaces — types that share fields
interface Node {
id: ID!
}
interface Timestamped {
createdAt: DateTime!
updatedAt: DateTime!
}
type Post implements Node & Timestamped {
id: ID!
title: String!
createdAt: DateTime!
updatedAt: DateTime!
}
# Resolver — must include __resolveType so the server knows which type a row is
const resolvers = {
Node: {
__resolveType(obj) {
if (obj.title) return 'Post';
if (obj.email) return 'User';
return null;
},
},
};
# 6) Unions — when types don't share fields
union SearchResult = User | Post | Comment
type Query {
search(q: String!): [SearchResult!]!
}
# Client must use inline fragments
query {
search(q: "docker") {
__typename
... on User { id name email }
... on Post { id title body }
... on Comment { id text author { name } }
}
}
# 7) Input types — for mutation arguments
input NewUserInput {
email: String!
name: String
role: Role = USER # default value
}
input PostFilter {
search: String
tags: [String!]
after: DateTime
before: DateTime
authorIds: [ID!]
}
type Mutation {
createUser(input: NewUserInput!): User!
}
type Query {
posts(filter: PostFilter, first: Int = 20, after: String): PostConnection!
}
# 8) Connection / Edge — Relay-style pagination
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
}
type PostEdge {
cursor: String!
node: Post!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
# 9) Nullability rules — read them carefully
# String = nullable string
# String! = non-null string
# [String] = nullable array of nullable strings
# [String!] = nullable array of non-null strings
# [String!]! = non-null array of non-null strings
# Server returning null when type says non-null = error. Bubble up nullable parents.
# 10) Type extensions — schema stitching / modular schemas
extend type Query {
me: User
}
extend type User {
subscription: Subscription
}
# 11) Schema directives
directive @auth(role: Role = USER) on FIELD_DEFINITION
directive @deprecated(reason: String) on FIELD_DEFINITION
type Query {
me: User! @auth(role: USER)
secrets: [String!]! @auth(role: ADMIN)
legacyField: String @deprecated(reason: "Use newField instead")
}
# 12) Code-first vs schema-first
# Schema-first (SDL files): .graphql files + resolver map. Tools: graphql-tools, codegen.
# Code-first (TypeScript): Pothos, Nexus, TypeGraphQL. Types flow from code.
# 13) Type generation — keep client + server in sync
# graphql-codegen reads the schema + ops, emits TS types.
# Set up:
# yarn add -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations
# codegen.ts:
# schema: 'schema.graphql'
# documents: 'src/**/*.graphql'
# generates: { 'src/types.ts': { plugins: ['typescript', 'typescript-operations'] } }
# 14) Best practices
# - Non-null EVERYTHING that's never null on success (id, createdAt) — better client ergonomics
# - Use ID for opaque identifiers — never reveal that they're integers
# - Pluralize lists: `posts`, not `postList`
# - Use input types for mutations — additive evolution
# - Pagination via Connection — first/after, not page/per_page
# - Default values on inputs reduce client noise
# - Mark deprecated, don't break — give clients a migration window
Why it matters
Non-null aggressively, use input types for mutations, paginate with Connection — the three habits that make a GraphQL schema feel professional from day one.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Discussion
Loading…