Skip to content

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.

PackagePlatformInstall
@effect-gql/nodeNode.jsnpm install @effect-gql/node
@effect-gql/bunBunnpm install @effect-gql/bun
@effect-gql/expressExpress.jsnpm install @effect-gql/express
@effect-gql/webWeb standardsnpm install @effect-gql/web

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 schema
const builder = GraphQLSchemaBuilder.empty.pipe(
query("hello", {
type: S.String,
resolve: () => Effect.succeed("Hello, World!")
})
)
// Convert to router and serve
const router = toRouter(builder, Layer.empty, { graphiql: true })
serve(router, Layer.empty, {
port: 4000,
onStart: (url) => console.log(`🚀 Server running at ${url}`)
})

The HttpRouter from @effect/platform is the central abstraction for HTTP handling. Effect GraphQL provides two ways to create a GraphQL router:

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
})

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
}
})

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 GraphiQL
const router = toRouter(builder, layer)
// Enable GraphiQL with defaults
const router = toRouter(builder, layer, { graphiql: true })
// Custom paths
const router = toRouter(builder, layer, {
path: "/api/graphql",
graphiql: {
path: "/playground",
endpoint: "/api/graphql"
}
})
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/bun"
const router = toRouter(builder, serviceLayer, { graphiql: true })
serve(router, serviceLayer, {
port: 4000,
onStart: (url) => console.log(`Server at ${url}`)
})
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)

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:

VariableDescriptionDefault
GRAPHQL_PATHGraphQL endpoint path/graphql
GRAPHIQL_ENABLEDEnable GraphiQL UIfalse
GRAPHIQL_PATHGraphiQL UI path/graphiql
GRAPHIQL_ENDPOINTGraphQL URL for GraphiQLSame as GRAPHQL_PATH
GRAPHQL_MAX_DEPTHMaximum query depth(unlimited)
GRAPHQL_MAX_COMPLEXITYMaximum complexity score(unlimited)
GRAPHQL_MAX_FIELDSMaximum field count(unlimited)
GRAPHQL_MAX_ALIASESMaximum alias count(unlimited)

Example:

Terminal window
GRAPHQL_PATH=/api/graphql
GRAPHIQL_ENABLED=true
GRAPHIQL_PATH=/playground
GRAPHQL_MAX_DEPTH=10
GRAPHQL_MAX_COMPLEXITY=1000

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 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.).

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.

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 })

For Kubernetes deployments, configure liveness and readiness probes:

apiVersion: v1
kind: Pod
spec:
containers:
- name: graphql-api
livenessProbe:
httpGet:
path: /health
port: 4000
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 4000
initialDelaySeconds: 5
periodSeconds: 5

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.

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: ["*"],
})

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
]
  • 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 allowedHeaders for Apollo clients

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 defaults
const router = toRouter(builder, layer, { graphiql: true })
// Access at: http://localhost:4000/graphiql
// Custom configuration
const router = toRouter(builder, layer, {
path: "/api/graphql",
graphiql: {
path: "/playground",
endpoint: "/api/graphql"
}
})
// Access at: http://localhost:4000/playground

The service layer you provide is used to resolve Effect service dependencies in your resolvers:

import { Context, Effect, Layer } from "effect"
// Define a service
class Database extends Context.Tag("Database")<Database, {
getUsers: () => Effect.Effect<User[]>
}>() {}
// Create the layer
const DatabaseLive = Layer.succeed(Database, {
getUsers: () => Effect.succeed([{ id: "1", name: "Alice" }])
})
// Use in resolver
const 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 router
const router = toRouter(builder, DatabaseLive, { graphiql: true })

HTTP-level errors are handled automatically:

  • Parse errors → 400 Bad Request
  • Validation errors → 400 Bad Request
  • Resolver errors → 200 OK with errors array (per GraphQL spec)
  • Unexpected errors → 400 Bad Request with generic message

For application-level errors, see the Error Handling guide.