Design a GraphQL Schema So Good, It'll Make REST APIs Cry - Part 1
Designing a robust, scalable GraphQL schema is critical for building production-ready APIs that can evolve with your application's needs. In this comprehensive guide, we'll walk through the process of crafting a GraphQL schema for a real-world application, highlighting best practices and considerations along the way.
If you are thinking how we could possibly cover all of the lovely intricacies associated with this topic in one go, you are right, we can't and so we are not! We have created an amazing series to take you through the nuances of working with GraphQL schemas.
Let's break our job into puzzle pieces. Let's start by simply creating designing a brand new schema!
If you're new to GraphQL Schema, check out our GraphQL Schema Tutorial to get up to speed with the basics.
The Power of GraphQL Schemas
A well-designed GraphQL schema serves as the blueprint for your entire API. It defines:
- The types of data available
- The relationships between those types
- The operations clients can perform (queries, mutations, subscriptions)
- The structure of requests and responses
Your schema acts as a contract between your backend and frontend teams. Once published, clients can rely on its structure, enabling them to build UIs with confidence. A thoughtful schema design upfront can save significant refactoring down the road.
Our Example Application: TechTalent
To illustrate schema design principles, let's imagine we're building TechTalent - a platform connecting tech companies with job seekers. Our application will allow:
- Companies to post job listings
- Candidates to create profiles and apply to jobs
- Recruiters to search candidates and manage applications
We'll design our schema step-by-step to support these core features.
Step 1: Identify Core Types
The first step is to identify the main entities in our domain. For TechTalent, our core types might include:
- Company
- JobListing
- Candidate
- Application
- Recruiter
Let's start by defining these as object types in our schema:
type Company {
id: ID!
name: String!
description: String
# More fields to come
}
type JobListing {
id: ID!
title: String!
description: String!
# More fields to come
}
type Candidate {
id: ID!
name: String!
email: String!
# More fields to come
}
type Application {
id: ID!
# More fields to come
}
type Recruiter {
id: ID!
name: String!
email: String!
# More fields to come
}
Notice we've only included a few basic fields at this stage. We'll flesh these out as we progress.
Step 2: Model Relationships
Next, we need to consider how these types relate to each other. In GraphQL, we model relationships by adding fields that reference other types. Let's update our types:
type Company {
id: ID!
name: String!
description: String
jobListings: [JobListing!]!
recruiters: [Recruiter!]!
}
type JobListing {
id: ID!
title: String!
description: String!
company: Company!
applications: [Application!]!
}
type Candidate {
id: ID!
name: String!
email: String!
applications: [Application!]!
}
type Application {
id: ID!
jobListing: JobListing!
candidate: Candidate!
status: ApplicationStatus!
}
type Recruiter {
id: ID!
name: String!
email: String!
company: Company!
}
enum ApplicationStatus {
PENDING
REVIEWED
REJECTED
ACCEPTED
}
We've now established the core relationships:
- Companies have job listings and recruiters
- Job listings belong to a company and have applications
- Candidates have applications
- Applications link a candidate to a job listing
- Recruiters belong to a company
Note the use of the ApplicationStatus
enum to represent the fixed set of possible statuses.
Step 3: Plan Query Operations
With our core types defined, let's consider what query operations our clients will need. We'll start with some basic CRUD (Create, Read, Update, Delete) operations:
type Query {
company(id: ID!): Company
jobListing(id: ID!): JobListing
candidate(id: ID!): Candidate
# List operations
companies: [Company!]!
jobListings(filters: JobListingFilters): [JobListing!]!
candidates(filters: CandidateFilters): [Candidate!]!
}
input JobListingFilters {
companyId: ID
title: String
# Add more filter options
}
input CandidateFilters {
skills: [String!]
experienceYears: Int
# Add more filter options
}
We've added basic queries to fetch individual entities by ID, as well as list queries for our main types. Notice the use of input
types for filters - this allows for more flexible and extensible querying.