Skip to main content

Chapter 10: Deployment

In this chapter, we'll explore various deployment strategies and best practices for GraphQL Yoga applications.

Deployment Options

Serverless Deployment

Vercel

Deploy to Vercel:

// api/graphql.ts
import { createYoga } from '@graphql-yoga/node'
import { schema } from '../../schema'

export default createYoga({
schema,
graphqlEndpoint: '/api/graphql'
})

// Configure Next.js
export const config = {
api: {
bodyParser: false
}
}

AWS Lambda

Deploy to AWS Lambda:

// lambda.ts
import { createYoga } from '@graphql-yoga/node'
import { schema } from './schema'

const yoga = createYoga({
schema
})

export const handler = async (event: any) => {
const response = await yoga.fetch('http://localhost:4000/graphql', {
method: event.httpMethod,
headers: event.headers,
body: event.body
})

return {
statusCode: response.status,
headers: Object.fromEntries(response.headers),
body: await response.text()
}
}

Container Deployment

Docker

Create a Dockerfile:

# Dockerfile
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .
RUN npm run build

EXPOSE 4000

CMD ["npm", "start"]

Build and run:

docker build -t graphql-yoga-app .
docker run -p 4000:4000 graphql-yoga-app

Kubernetes

Create Kubernetes manifests:

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: graphql-api
spec:
replicas: 3
selector:
matchLabels:
app: graphql-api
template:
metadata:
labels:
app: graphql-api
spec:
containers:
- name: graphql-api
image: graphql-yoga-app:latest
ports:
- containerPort: 4000
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-secrets
key: url
---
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: graphql-api
spec:
selector:
app: graphql-api
ports:
- port: 80
targetPort: 4000
type: LoadBalancer

Environment Configuration

Environment Variables

Manage environment variables:

// config.ts
import { z } from 'zod'

const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']),
PORT: z.string().transform(Number),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
CORS_ORIGIN: z.string().url()
})

export const config = envSchema.parse(process.env)

Secrets Management

Use a secrets manager:

// secrets.ts
import { SecretsManager } from 'aws-sdk'

const secretsManager = new SecretsManager()

export async function getSecret(name: string) {
const secret = await secretsManager.getSecretValue({
SecretId: name
}).promise()

return JSON.parse(secret.SecretString || '{}')
}

Production Best Practices

Security

Implement security measures:

// server.ts
import { createYoga } from '@graphql-yoga/node'
import { useResponseCache } from '@envelop/response-cache'
import { useValidation } from '@envelop/validation'
import { useErrorHandler } from '@envelop/error-handler'

const yoga = createYoga({
schema,
plugins: [
useResponseCache({
ttl: 60 * 1000
}),
useValidation({
rules: [
maxDepth(3),
maxComplexity(1000),
noIntrospection()
]
}),
useErrorHandler({
formatError: (error) => {
// Don't expose internal errors
if (error.originalError instanceof Error) {
return {
message: 'Internal server error',
code: 'INTERNAL_SERVER_ERROR'
}
}
return error
}
})
]
})

CORS Configuration

Configure CORS:

// server.ts
const yoga = createYoga({
schema,
cors: {
origin: process.env.CORS_ORIGIN,
credentials: true,
methods: ['GET', 'POST', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
}
})

Rate Limiting

Implement rate limiting:

// server.ts
import { createYoga } from '@graphql-yoga/node'
import { useRateLimit } from '@envelop/rate-limit'

const yoga = createYoga({
schema,
plugins: [
useRateLimit({
// 100 requests per minute
max: 100,
window: '1m',
// Custom key based on IP and user
keyGenerator: (context) => {
const ip = context.request.headers.get('x-forwarded-for')
const userId = context.user?.id
return `${ip}:${userId}`
}
})
]
})

Monitoring and Logging

Application Monitoring

Set up monitoring:

// server.ts
import { createYoga } from '@graphql-yoga/node'
import { usePrometheus } from '@envelop/prometheus'
import { useSentry } from '@envelop/sentry'

const yoga = createYoga({
schema,
plugins: [
usePrometheus({
path: '/metrics',
metrics: {
queryDuration: {
type: 'histogram',
help: 'Duration of GraphQL queries'
},
errorCount: {
type: 'counter',
help: 'Number of GraphQL errors'
}
}
}),
useSentry({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV
})
]
})

Logging

Implement structured logging:

// logger.ts
import pino from 'pino'

export const logger = pino({
level: process.env.LOG_LEVEL || 'info',
formatters: {
level: (label) => {
return { level: label }
}
}
})

// server.ts
const yoga = createYoga({
schema,
plugins: [
{
onExecute: ({ args }) => {
logger.info({
operation: args.operationName,
variables: args.variableValues
})
},
onExecuteDone: ({ result }) => {
if (result.errors) {
logger.error({
errors: result.errors
})
}
}
}
]
})

CI/CD Pipeline

GitHub Actions

Set up CI/CD with GitHub Actions:

# .github/workflows/ci.yml
name: CI

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2

- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '18'

- name: Install dependencies
run: npm ci

- name: Run tests
run: npm test

- name: Build
run: npm run build

deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'

steps:
- uses: actions/checkout@v2

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1

- name: Deploy to ECS
run: |
aws ecs update-service --cluster prod --service graphql-api --force-new-deployment

Summary

In this chapter, we've covered:

  • Deployment options (serverless, containers)
  • Environment configuration
  • Production best practices
  • Monitoring and logging
  • CI/CD pipeline setup

These deployment strategies and practices will help you run your GraphQL API reliably in production. In the next chapter, we'll conclude with a summary of what we've learned.