Server Integration
Effect GraphQL provides platform-specific packages for running your GraphQL server. Each package integrates seamlessly with @effect/platform and provides consistent APIs across different runtimes.
Available Packages
Section titled “Available Packages”| Package | Platform | Install |
|---|---|---|
@effect-gql/node | Node.js | npm install @effect-gql/node |
@effect-gql/bun | Bun | npm install @effect-gql/bun |
@effect-gql/express | Express.js | npm install @effect-gql/express |
@effect-gql/web | Web standards | npm install @effect-gql/web |
Quick Start with Node.js
Section titled “Quick Start with Node.js”The fastest way to get a GraphQL server running:
import { GraphQLSchemaBuilder, query, toRouter } from "@effect-gql/core"import { serve } from "@effect-gql/node"import { Effect, Layer } from "effect"import * as S from "effect/Schema"
// Build your schemaconst builder = GraphQLSchemaBuilder.empty.pipe( query("hello", { type: S.String, resolve: () => Effect.succeed("Hello, World!") }))
// Convert to router and serveconst router = toRouter(builder, Layer.empty, { graphiql: true })
serve(router, Layer.empty, { port: 4000, onStart: (url) => console.log(`🚀 Server running at ${url}`)})Core Concepts
Section titled “Core Concepts”The Router
Section titled “The Router”The HttpRouter from @effect/platform is the central abstraction for HTTP handling. Effect GraphQL provides two ways to create a GraphQL router:
Using toRouter (Recommended)
Section titled “Using toRouter (Recommended)”Converts a GraphQLSchemaBuilder directly to an HttpRouter:
import { GraphQLSchemaBuilder, toRouter } from "@effect-gql/core"import { Layer } from "effect"
const builder = GraphQLSchemaBuilder.empty.pipe( // ... define your schema)
const router = toRouter(builder, serviceLayer, { path: "/graphql", // GraphQL endpoint (default: "/graphql") graphiql: true // Enable GraphiQL UI})Using makeGraphQLRouter
Section titled “Using makeGraphQLRouter”If you already have a built GraphQLSchema:
import { makeGraphQLRouter } from "@effect-gql/core"
const schema = builder.buildSchema()
const router = makeGraphQLRouter(schema, serviceLayer, { path: "/graphql", graphiql: { path: "/graphiql", // Where GraphiQL is served endpoint: "/graphql" // Where GraphiQL sends requests }})Router Configuration
Section titled “Router Configuration”Both toRouter and makeGraphQLRouter accept a configuration object:
interface GraphQLRouterConfigInput { // Path for GraphQL endpoint (default: "/graphql") path?: string
// GraphiQL configuration graphiql?: boolean | { path?: string // Path for GraphiQL UI (default: "/graphiql") endpoint?: string // GraphQL endpoint URL (default: same as path) }
// Query complexity limiting (see Complexity Limiting guide) complexity?: { maxDepth?: number // Maximum query nesting depth maxComplexity?: number // Maximum total complexity score maxFields?: number // Maximum number of fields maxAliases?: number // Maximum number of aliases }}Examples:
// Minimal - GraphQL at /graphql, no GraphiQLconst router = toRouter(builder, layer)
// Enable GraphiQL with defaultsconst router = toRouter(builder, layer, { graphiql: true })
// Custom pathsconst router = toRouter(builder, layer, { path: "/api/graphql", graphiql: { path: "/playground", endpoint: "/api/graphql" }})Platform-Specific Usage
Section titled “Platform-Specific Usage”Node.js
Section titled “Node.js”import { toRouter } from "@effect-gql/core"import { serve } from "@effect-gql/node"
const router = toRouter(builder, serviceLayer, { graphiql: true })
serve(router, serviceLayer, { port: 4000, host: "0.0.0.0", onStart: (url) => console.log(`Server at ${url}`)})import { toRouter } from "@effect-gql/core"import { serve } from "@effect-gql/node"
const schema = builder.buildSchema()const router = toRouter(builder, serviceLayer, { graphiql: true })
serve(router, serviceLayer, { port: 4000, subscriptions: { schema, path: "/graphql", onConnect: (params) => Effect.gen(function* () { // Validate auth token const user = yield* AuthService.validateToken(params.authToken) return { user } }) }, onStart: (url) => console.log(`Server at ${url}`)})import { HttpApp } from "@effect/platform"import { createServer } from "node:http"import { createGraphQLWSServer } from "@effect-gql/node"
const schema = builder.buildSchema()const { handler } = HttpApp.toWebHandlerLayer(router, serviceLayer)
const httpServer = createServer(async (req, res) => { // Handle HTTP requests with Effect router const response = await handler(new Request(...)) // Write response...})
// Add WebSocket supportconst { handleUpgrade, close } = createGraphQLWSServer(schema, serviceLayer)httpServer.on("upgrade", (req, socket, head) => handleUpgrade(req, socket, head))
httpServer.listen(4000)import { toRouter } from "@effect-gql/core"import { serve } from "@effect-gql/bun"
const router = toRouter(builder, serviceLayer, { graphiql: true })
serve(router, serviceLayer, { port: 4000, onStart: (url) => console.log(`Server at ${url}`)})Express
Section titled “Express”import express from "express"import { graphqlMiddleware } from "@effect-gql/express"
const app = express()
const schema = builder.buildSchema()
app.use("/graphql", graphqlMiddleware(schema, serviceLayer))
app.listen(4000)Environment Configuration
Section titled “Environment Configuration”Load router configuration from environment variables using GraphQLRouterConfigFromEnv:
import { Effect, Layer } from "effect"import { GraphQLRouterConfigFromEnv, makeGraphQLRouter } from "@effect-gql/core"
const program = Effect.gen(function* () { const config = yield* GraphQLRouterConfigFromEnv const router = makeGraphQLRouter(schema, layer, config) // Use router...})Environment Variables:
| Variable | Description | Default |
|---|---|---|
GRAPHQL_PATH | GraphQL endpoint path | /graphql |
GRAPHIQL_ENABLED | Enable GraphiQL UI | false |
GRAPHIQL_PATH | GraphiQL UI path | /graphiql |
GRAPHIQL_ENDPOINT | GraphQL URL for GraphiQL | Same as GRAPHQL_PATH |
GRAPHQL_MAX_DEPTH | Maximum query depth | (unlimited) |
GRAPHQL_MAX_COMPLEXITY | Maximum complexity score | (unlimited) |
GRAPHQL_MAX_FIELDS | Maximum field count | (unlimited) |
GRAPHQL_MAX_ALIASES | Maximum alias count | (unlimited) |
Example:
GRAPHQL_PATH=/api/graphqlGRAPHIQL_ENABLED=trueGRAPHIQL_PATH=/playgroundGRAPHQL_MAX_DEPTH=10GRAPHQL_MAX_COMPLEXITY=1000Composing Routes
Section titled “Composing Routes”The GraphQL router can be composed with other routes:
import { HttpRouter, HttpServerResponse } from "@effect/platform"import { toRouter } from "@effect-gql/core"
const graphqlRouter = toRouter(builder, serviceLayer, { graphiql: true })
const app = HttpRouter.empty.pipe( // Health check endpoint HttpRouter.get("/health", HttpServerResponse.json({ status: "ok" })),
// Static files HttpRouter.get("/docs/*", serveStaticFiles),
// GraphQL routes HttpRouter.concat(graphqlRouter))Health Checks
Section titled “Health Checks”Health checks verify your server is running and can serve requests. Rather than implementing health checks in the GraphQL framework, we recommend using platform-level patterns. This gives you full control over health check behavior for your specific deployment environment (Kubernetes, AWS ALB, etc.).
GraphQL-Level Health Check
Section titled “GraphQL-Level Health Check”The simplest approach is using a GraphQL query as your health check. Every GraphQL server supports:
GET /graphql?query={__typename}This verifies the entire GraphQL execution pipeline is working. Add the apollo-require-preflight: true header if using CSRF protection.
Platform-Specific Patterns
Section titled “Platform-Specific Patterns”Use HttpRouter to add a health endpoint before the GraphQL router:
import { HttpRouter, HttpServerResponse } from "@effect/platform"import { toRouter } from "@effect-gql/core"import { serve } from "@effect-gql/node" // or @effect-gql/bun
const graphqlRouter = toRouter(builder, serviceLayer, { graphiql: true })
const app = HttpRouter.empty.pipe( // Simple health check HttpRouter.get("/health", HttpServerResponse.json({ status: "ok" })),
// Readiness check with service verification HttpRouter.get("/ready", Effect.gen(function* () { const db = yield* Database yield* db.ping() return yield* HttpServerResponse.json({ status: "ready" }) }).pipe( Effect.catchAll(() => HttpServerResponse.json({ status: "not ready" }, { status: 503 }) ) ) ),
// GraphQL routes HttpRouter.concat(graphqlRouter))
serve(app, serviceLayer, { port: 4000 })Add health routes directly to your Express app:
import express from "express"import { graphqlMiddleware } from "@effect-gql/express"
const app = express()
// Simple liveness checkapp.get("/health", (req, res) => { res.json({ status: "ok" })})
// Readiness check with async verificationapp.get("/ready", async (req, res) => { try { await checkDatabaseConnection() res.json({ status: "ready" }) } catch (error) { res.status(503).json({ status: "not ready" }) }})
// GraphQL endpointapp.use("/graphql", graphqlMiddleware(schema, serviceLayer))
app.listen(4000)For Cloudflare Workers, Deno, or other web-standard environments:
import { toHandler } from "@effect-gql/web"
const graphqlHandler = toHandler(builder, serviceLayer)
export default { async fetch(request: Request): Promise<Response> { const url = new URL(request.url)
// Health check if (url.pathname === "/health") { return new Response(JSON.stringify({ status: "ok" }), { headers: { "content-type": "application/json" } }) }
// GraphQL if (url.pathname === "/graphql") { return graphqlHandler(request) }
return new Response("Not Found", { status: 404 }) }}Kubernetes Example
Section titled “Kubernetes Example”For Kubernetes deployments, configure liveness and readiness probes:
apiVersion: v1kind: Podspec: containers: - name: graphql-api livenessProbe: httpGet: path: /health port: 4000 initialDelaySeconds: 5 periodSeconds: 10 readinessProbe: httpGet: path: /ready port: 4000 initialDelaySeconds: 5 periodSeconds: 5CORS (Cross-Origin Resource Sharing)
Section titled “CORS (Cross-Origin Resource Sharing)”CORS is HTTP infrastructure that should be configured at the platform level, not in the GraphQL framework. This gives you full control over origins, methods, and credentials for your specific deployment.
Platform-Specific Patterns
Section titled “Platform-Specific Patterns”Use Effect’s HttpMiddleware.cors for declarative CORS configuration:
import { HttpMiddleware, HttpRouter } from "@effect/platform"import { toRouter } from "@effect-gql/core"import { serve } from "@effect-gql/node" // or @effect-gql/bun
const graphqlRouter = toRouter(builder, serviceLayer, { graphiql: true })
const app = graphqlRouter.pipe( HttpRouter.use( HttpMiddleware.cors({ allowedOrigins: ["https://myapp.com", "https://studio.apollographql.com"], allowedMethods: ["GET", "POST", "OPTIONS"], allowedHeaders: ["Content-Type", "Authorization", "Apollo-Require-Preflight"], allowCredentials: true, maxAge: 86400, // 24 hours }) ))
serve(app, serviceLayer, { port: 4000 })For development, allow all origins:
const corsMiddleware = HttpMiddleware.cors({ allowedOrigins: ["*"], allowedMethods: ["GET", "POST", "OPTIONS"], allowedHeaders: ["*"],})Use the popular cors package:
npm install cors @types/corsimport express from "express"import cors from "cors"import { graphqlMiddleware } from "@effect-gql/express"
const app = express()
// Configure CORSapp.use(cors({ origin: ["https://myapp.com", "https://studio.apollographql.com"], methods: ["GET", "POST", "OPTIONS"], allowedHeaders: ["Content-Type", "Authorization", "Apollo-Require-Preflight"], credentials: true, maxAge: 86400,}))
// GraphQL endpointapp.use("/graphql", graphqlMiddleware(schema, serviceLayer))
app.listen(4000)Handle CORS manually for Cloudflare Workers, Deno, etc.:
import { toHandler } from "@effect-gql/web"
const graphqlHandler = toHandler(builder, serviceLayer)
const corsHeaders = { "Access-Control-Allow-Origin": "https://myapp.com", "Access-Control-Allow-Methods": "GET, POST, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization", "Access-Control-Max-Age": "86400",}
export default { async fetch(request: Request): Promise<Response> { // Handle preflight if (request.method === "OPTIONS") { return new Response(null, { headers: corsHeaders }) }
const url = new URL(request.url)
if (url.pathname === "/graphql") { const response = await graphqlHandler(request) // Add CORS headers to response const headers = new Headers(response.headers) Object.entries(corsHeaders).forEach(([k, v]) => headers.set(k, v)) return new Response(response.body, { status: response.status, headers, }) }
return new Response("Not Found", { status: 404 }) }}Apollo Studio Integration
Section titled “Apollo Studio Integration”To use Apollo Studio or Apollo Sandbox with your local server, add their origin to your CORS configuration:
allowedOrigins: [ "https://studio.apollographql.com", // ... your app origins]Security Considerations
Section titled “Security Considerations”- Never use
*for origins in production - always specify allowed domains - Be careful with
credentials: true- only enable if your app requires cookies/auth headers - Consider per-environment configuration - use environment variables for origin lists
- Apollo-Require-Preflight header - include this in
allowedHeadersfor Apollo clients
GraphiQL
Section titled “GraphiQL”When enabled, GraphiQL provides an interactive IDE for exploring your GraphQL API:
- Syntax highlighting for queries
- Auto-complete with schema introspection
- Query history for recent operations
- Documentation explorer for browsing types
The GraphiQL UI is loaded from CDN (unpkg.com) with no local dependencies.
// Enable with defaultsconst router = toRouter(builder, layer, { graphiql: true })// Access at: http://localhost:4000/graphiql
// Custom configurationconst router = toRouter(builder, layer, { path: "/api/graphql", graphiql: { path: "/playground", endpoint: "/api/graphql" }})// Access at: http://localhost:4000/playgroundService Layer Integration
Section titled “Service Layer Integration”The service layer you provide is used to resolve Effect service dependencies in your resolvers:
import { Context, Effect, Layer } from "effect"
// Define a serviceclass Database extends Context.Tag("Database")<Database, { getUsers: () => Effect.Effect<User[]>}>() {}
// Create the layerconst DatabaseLive = Layer.succeed(Database, { getUsers: () => Effect.succeed([{ id: "1", name: "Alice" }])})
// Use in resolverconst builder = GraphQLSchemaBuilder.empty.pipe( query("users", { type: S.Array(UserSchema), resolve: () => Effect.gen(function* () { const db = yield* Database return yield* db.getUsers() }) }))
// Provide layer when creating routerconst router = toRouter(builder, DatabaseLive, { graphiql: true })Error Handling
Section titled “Error Handling”HTTP-level errors are handled automatically:
- Parse errors → 400 Bad Request
- Validation errors → 400 Bad Request
- Resolver errors → 200 OK with
errorsarray (per GraphQL spec) - Unexpected errors → 400 Bad Request with generic message
For application-level errors, see the Error Handling guide.
Next Steps
Section titled “Next Steps”- WebSocket Subscriptions - Real-time data with graphql-ws
- Query Complexity Limiting - Protect against expensive queries
- DataLoader Integration - Batch and cache database queries
- Error Handling - Structured error responses