GraphQL for REST Users
If you're experienced with REST APIs, this guide will help you understand GraphQL by relating it to familiar REST concepts and showing how to leverage your existing knowledge in this new paradigm.
Core Conceptual Differences
Single Endpoint vs Multiple Endpoints
REST APIs:
GET /api/v1/users/123
GET /api/v1/users/123/orders
GET /api/v1/orders/456/items
POST /api/v1/users
PUT /api/v1/users/123
DELETE /api/v1/users/123GraphQL:
POST /graphqlAll operations go through a single endpoint, with the operation type and data requirements specified in the request body.
HTTP Methods vs Operation Types
REST: Uses HTTP methods to indicate operation type
GETfor readingPOSTfor creatingPUT/PATCHfor updatingDELETEfor removing
GraphQL: Uses operation types in the query
queryfor reading datamutationfor modifying datasubscriptionfor real-time updates
Request/Response Comparison
Fetching User Data
REST Request:
GET /api/v1/users/123
Accept: application/jsonREST Response:
{
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"phone": "555-1234",
"address": {
"street": "123 Main St",
"city": "Springfield",
"zip": "12345"
},
"preferences": {
"newsletter": true,
"notifications": false
},
"created_at": "2024-01-15T10:30:00Z"
}GraphQL Request:
POST /graphql
Content-Type: application/json
{
"query": "{ getUser(id: 123) { id name email } }"
}GraphQL Response:
{
"data": {
"getUser": {
"id": "123",
"name": "John Doe",
"email": "john@example.com"
}
}
}The Over-fetching Problem
In the REST example above, you received all user data even if you only needed the name and email. GraphQL solves this by letting you specify exactly what fields you need.
The Under-fetching Problem
REST: Multiple requests needed
GET /api/v1/print-jobs/456 # Get job details
GET /api/v1/customers/789 # Get customer info
GET /api/v1/devices/101 # Get assigned deviceGraphQL: Single request
POST /graphql
Content-Type: application/json
{
"query": "{
getPrintJob(id: 456) {
id
title
status
customer {
name
email
}
assignedDevice {
name
status
currentLoad
}
}
}"
}CRUD Operations Comparison
Reading Data
REST:
# Get all print jobs
GET /api/v1/print-jobs
# Get specific job
GET /api/v1/print-jobs/123
# Get jobs with filters
GET /api/v1/print-jobs?status=PENDING&limit=10&offset=20GraphQL:
# Get all print jobs with specific fields
{
printJobs {
id
title
status
}
}
# Get specific job
{
getPrintJob(id: "123") {
id
title
status
customer {
name
}
}
}
# Get jobs with filters and pagination
{
printJobs(
filter: { status: PENDING }
first: 10
after: "cursor123"
) {
edges {
node {
id
title
status
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}Creating Data
REST:
POST /api/v1/print-jobs
Content-Type: application/json
{
"title": "Business Cards",
"customerId": 456,
"priority": "HIGH"
}GraphQL:
mutation CreatePrintJob {
createPrintJob(input: {
title: "Business Cards"
customerId: "456"
priority: HIGH
}) {
result {
id
title
status
}
errors {
message
field
}
}
}Updating Data
REST:
PUT /api/v1/print-jobs/123
Content-Type: application/json
{
"status": "COMPLETED",
"completedAt": "2024-01-15T15:30:00Z"
}GraphQL:
mutation UpdatePrintJob {
updatePrintJob(
id: "123"
input: {
status: COMPLETED
completedAt: "2024-01-15T15:30:00Z"
}
) {
result {
id
status
completedAt
}
errors {
message
}
}
}Deleting Data
REST:
DELETE /api/v1/print-jobs/123GraphQL:
mutation DeletePrintJob {
destroyPrintJob(id: "123") {
result {
id
title
}
errors {
message
}
}
}Error Handling Comparison
REST Error Response
HTTP/1.1 404 Not Found
Content-Type: application/json
{
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "Print job with ID 123 not found",
"details": {
"resource": "print_job",
"id": "123"
}
}
}GraphQL Error Response
{
"data": {
"getPrintJob": null
},
"errors": [
{
"message": "Print job with ID 123 not found",
"extensions": {
"code": "PRINT_JOB_NOT_FOUND",
"id": "123"
},
"path": ["getPrintJob"]
}
]
}Key Differences:
- GraphQL always returns HTTP 200 (unless there's a server error)
- Errors are part of the response body, not HTTP status codes
- Partial data can be returned even when some fields error
- Multiple errors can be returned for different fields
Caching Strategies
REST Caching
# REST responses can use HTTP caching headers
HTTP/1.1 200 OK
Cache-Control: max-age=300, public
ETag: "abc123"
Last-Modified: Mon, 15 Jan 2024 10:30:00 GMT
{
"id": 123,
"name": "John Doe"
}GraphQL Caching
GraphQL caching is more complex because:
- Single endpoint means HTTP cache-control headers aren't as effective
- Different queries to the same endpoint need different caching strategies
Solutions:
- Normalized caching: Libraries like Apollo Client cache individual objects
- Persisted queries: Cache query strings on the server
- Field-level caching: Cache at the resolver level
// Apollo Client normalized cache
{
"User:123": {
"id": "123",
"name": "John Doe",
"email": "john@example.com"
},
"PrintJob:456": {
"id": "456",
"title": "Business Cards",
"customer": { "__ref": "User:123" }
}
}Pagination Patterns
REST Pagination
Offset-based:
GET /api/v1/print-jobs?limit=20&offset=40Cursor-based:
GET /api/v1/print-jobs?limit=20&cursor=abc123GraphQL Pagination
GraphQL standardizes on cursor-based pagination with the Relay connection specification:
{
printJobs(first: 20, after: "cursor123") {
edges {
node {
id
title
status
}
cursor
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}Versioning Strategies
REST API Versioning
URL Versioning:
/api/v1/users
/api/v2/usersHeader Versioning:
GET /api/users
Accept: application/vnd.api+json;version=2GraphQL Versioning
GraphQL encourages versionless APIs through field evolution:
type User {
id: ID!
name: String!
email: String!
# Add new fields without breaking existing queries
phone: String
# Deprecate fields instead of removing them
username: String @deprecated(reason: "Use 'name' field instead")
}Benefits:
- No need to maintain multiple API versions
- Clients can migrate gradually
- Backward compatibility is maintained
Real-time Updates
REST Approaches
Polling:
// Check for updates every 5 seconds
setInterval(async () => {
const response = await fetch('/api/v1/print-jobs/123');
const job = await response.json();
updateUI(job);
}, 5000);WebSockets:
const ws = new WebSocket('ws://localhost:8080/job-updates');
ws.onmessage = (event) => {
const update = JSON.parse(event.data);
updateUI(update);
};Server-Sent Events:
const eventSource = new EventSource('/api/v1/jobs/123/events');
eventSource.onmessage = (event) => {
const update = JSON.parse(event.data);
updateUI(update);
};GraphQL Subscriptions
subscription PrintJobUpdates {
printJobUpdated(filter: { id: "123" }) {
id
title
status
progress
estimatedCompletion
}
}Advantages:
- Same query language for real-time data
- Built into the GraphQL specification
- Integrated with existing type system
- Client can specify exactly what data to receive in updates
Migration Strategies
1. GraphQL Wrapper Approach
Start by wrapping existing REST endpoints with GraphQL:
type Query {
# New GraphQL-native endpoint
printJobs(filter: PrintJobFilter): [PrintJob!]!
# Wrapper around existing REST endpoint
legacyGetPrintJobs(
status: String
limit: Int
offset: Int
): LegacyPrintJobResponse
}2. Gradual Field Migration
Move fields from REST to GraphQL gradually:
type PrintJob {
# Already migrated to GraphQL
id: ID!
title: String!
status: JobStatus!
# Still calls REST endpoint internally
customer: Customer @restField(endpoint: "/api/v1/customers/{customerId}")
}3. Parallel APIs
Run both REST and GraphQL APIs simultaneously:
// REST client code (existing)
const job = await restClient.get(`/api/v1/print-jobs/${id}`);
// GraphQL client code (new)
const { data } = await graphqlClient.query({
query: gql`
query GetPrintJob($id: ID!) {
getPrintJob(id: $id) {
id
title
status
}
}
`,
variables: { id }
});Common REST Patterns in GraphQL
1. Resource Relationships
REST: Multiple requests or embedded resources
GET /api/v1/print-jobs/123
GET /api/v1/customers/456
GET /api/v1/print-jobs/123?include=customer,filesGraphQL: Natural relationship traversal
{
getPrintJob(id: "123") {
title
customer {
name
email
}
files {
filename
size
}
}
}2. Bulk Operations
REST: Batch endpoints
POST /api/v1/print-jobs/bulk
Content-Type: application/json
{
"operations": [
{ "action": "create", "data": {...} },
{ "action": "update", "id": 123, "data": {...} }
]
}GraphQL: Multiple mutations in one request
mutation BulkOperations {
createJob1: createPrintJob(input: {...}) {
result { id }
errors { message }
}
updateJob2: updatePrintJob(id: "123", input: {...}) {
result { id status }
errors { message }
}
}3. Search and Filtering
REST: Query parameters
GET /api/v1/print-jobs?status=PENDING&customer=123&sort=created_at&order=descGraphQL: Structured input types
{
printJobs(
filter: {
status: PENDING
customerId: "123"
}
sort: {
field: CREATED_AT
direction: DESC
}
) {
id
title
status
}
}CoCore-Specific Examples
Device Management
REST Approach:
GET /api/v1/devices
GET /api/v1/devices/printer1/status
GET /api/v1/devices/printer1/jobs
GET /api/v1/devices/printer1/maintenanceGraphQL Approach:
{
devices {
id
name
type
status
currentJobs {
id
title
progress
}
maintenanceSchedule {
nextService
lastService
alerts {
type
message
}
}
}
}Order Processing
REST: Sequential API calls
// 1. Create order
const order = await post('/api/v1/orders', orderData);
// 2. Add items
for (const item of items) {
await post(`/api/v1/orders/${order.id}/items`, item);
}
// 3. Calculate pricing
const pricing = await post(`/api/v1/orders/${order.id}/calculate`);
// 4. Apply discounts
await put(`/api/v1/orders/${order.id}`, { discount: pricing.discount });GraphQL: Single mutation
mutation CreateCompleteOrder {
createOrder(input: {
customer: "123"
items: [
{ productId: "456", quantity: 500 }
{ productId: "789", quantity: 1000 }
]
applyAutoDiscounts: true
}) {
result {
id
total
items {
product { name }
quantity
unitPrice
total
}
discounts {
type
amount
}
}
errors {
message
field
}
}
}Performance Considerations
REST Performance Issues
N+1 Problem:
// Get all print jobs
const jobs = await get('/api/v1/print-jobs');
// Then get customer for each job (N+1 queries)
for (const job of jobs) {
job.customer = await get(`/api/v1/customers/${job.customerId}`);
}Over-fetching:
// Only need job titles, but get full objects
const jobs = await get('/api/v1/print-jobs');
const titles = jobs.map(job => job.title);GraphQL Solutions
Efficient Data Loading:
# DataLoader prevents N+1 queries behind the scenes
{
printJobs {
title
customer {
name # Batched loading of customers
}
}
}Request Exactly What You Need:
{
printJobs {
title # Only fetch titles
}
}Best Practices for REST Developers
1. Think in Terms of Data Requirements
REST Mindset: "What endpoint do I call?" GraphQL Mindset: "What data do I need?"
2. Use Fragments for Reusability
Similar to REST client helper functions:
fragment JobSummary on PrintJob {
id
title
status
priority
estimatedCompletion
}
query GetActiveJobs {
activeJobs: printJobs(filter: { status: [PENDING, PRINTING] }) {
...JobSummary
}
}
query GetCompletedJobs {
completedJobs: printJobs(filter: { status: COMPLETED }) {
...JobSummary
completedAt
}
}3. Handle Errors Gracefully
Unlike REST where you might have global error handlers based on HTTP status codes, GraphQL requires more granular error handling:
const { data, errors } = await graphqlClient.query({
query: GET_PRINT_JOB,
variables: { id: '123' }
});
if (errors) {
// Handle GraphQL errors
errors.forEach(error => {
console.error(`Error in ${error.path}: ${error.message}`);
});
}
if (data?.getPrintJob) {
// Handle successful data
updateUI(data.getPrintJob);
} else {
// Handle null data (possibly due to errors)
showErrorMessage("Job not found");
}4. Leverage Type Safety
GraphQL's strong type system provides benefits similar to REST API documentation, but enforced at runtime:
// Generated types from GraphQL schema
interface PrintJob {
id: string;
title: string;
status: JobStatus;
customer: Customer;
}
// Type-safe queries
const job: PrintJob = await client.request(GET_PRINT_JOB, { id: '123' });Conclusion
GraphQL offers significant advantages over REST for complex applications like CoCore:
- Efficiency: Fetch exactly what you need in a single request
- Flexibility: Clients control data requirements
- Real-time: Built-in subscription support
- Type Safety: Strong schema-based type system
- Developer Experience: Excellent tooling and introspection
Your REST experience translates well to GraphQL—many concepts like resources, CRUD operations, and data relationships are similar. The main shift is from endpoint-thinking to data-requirement thinking.
Start with simple queries, gradually migrate existing REST endpoints, and leverage GraphQL's flexibility to build more efficient integrations with CoCore's manufacturing systems.
Next Steps
- Learn about GraphQL tooling for development
- Explore the GraphQL introduction for deeper concepts
- Try converting your existing REST API calls to GraphQL queries
- Experiment with CoCore's GraphQL playground
- Set up your first GraphQL subscriptions for real-time updates