@stonecrop/nuxt-grafserv
v0.7.9
Published
Pluggable Grafserv GraphQL server as Nuxt Module
Readme
Nuxt Grafserv
Pluggable Grafserv GraphQL server as a Nuxt Module. Uses the Grafast execution engine for high-performance GraphQL.
Features
- 🚀 Grafserv Server Integration
- ⚡️ Grafast Execution Engine (faster than
graphql-js) - 🔄 Schema Stitching Support
- 🔍 Graphile Preset System for Advanced Configuration
- 📝 TypeScript Support
- 🔍 GraphiQL/Ruru Interface
- ⚡️ Hot Module Reloading
- 🎯 Separate Route Handlers for GraphQL/UI and Static Assets
Architecture
This module uses modern Grafserv patterns with three key components:
- Preset-Based Configuration: Leverages Graphile's preset system for extensibility and plugin support
- Separate Route Handlers: Two dedicated handlers for GraphQL operations/UI and static assets
- Objects Structure: Uses Grafast's modern
objects/interfaces/enumsschema building pattern for better type safety
The module automatically registers these handlers:
{url}- Unified GraphQL operations and Ruru UI endpoint/ruru-static/**- Static assets for the Ruru IDE
Quick Setup
PostGraphile Integration (Recommended)
For PostGraphile users, this is the recommended configuration approach:
- Add dependencies:
# Using pnpm
pnpm add @stonecrop/nuxt-grafserv postgraphile
# Using yarn
yarn add @stonecrop/nuxt-grafserv postgraphile
# Using npm
npm install @stonecrop/nuxt-grafserv postgraphile- Create your preset file
server/graphile.preset.ts:
import { PostGraphileAmberPreset } from 'postgraphile/presets/amber'
import { makePgService } from 'postgraphile/adaptors/pg'
const preset = {
extends: [PostGraphileAmberPreset],
pgServices: [
makePgService({
connectionString: process.env.DATABASE_URL || 'postgresql://localhost/mydb',
schemas: ['public'],
}),
],
}
export default preset- Configure in
nuxt.config.ts:
export default defineNuxtConfig({
modules: ['@stonecrop/nuxt-grafserv'],
grafserv: {
type: 'postgraphile', // Required: specify configuration type
preset: './server/graphile.preset.ts', // Path to preset file
url: '/graphql',
graphiql: true,
}
})Custom Schema Configuration
For custom GraphQL schemas with your own resolvers:
- Add dependency:
pnpm add @stonecrop/nuxt-grafserv- Configure in
nuxt.config.ts:
export default defineNuxtConfig({
modules: ['@stonecrop/nuxt-grafserv'],
grafserv: {
type: 'schema', // Required: specify configuration type
schema: 'server/**/*.graphql',
resolvers: 'server/resolvers.ts',
url: '/graphql',
graphiql: true,
}
})Configuration
The module supports two configuration types using a discriminated union pattern:
PostGraphile Configuration
Use type: 'postgraphile' for PostGraphile-based GraphQL APIs:
| Option | Type | Required | Description |
|--------|------|----------|-------------|
| type | 'postgraphile' | ✅ | Configuration type discriminator |
| preset | string | ✅ | Path to PostGraphile preset file (e.g., './server/graphile.preset.ts') |
| url | string | ❌ | GraphQL endpoint URL (default: '/graphql/') |
| graphiql | boolean | ❌ | Enable GraphiQL IDE (default: true in dev, false in prod) |
Important: The preset must be a file path, not an inline object. See "Why File-Based Presets?" section below.
Example:
// nuxt.config.ts
export default defineNuxtConfig({
grafserv: {
type: 'postgraphile',
preset: './server/graphile.preset.ts',
url: '/graphql',
graphiql: true,
}
})// server/graphile.preset.ts
import { PostGraphileAmberPreset } from 'postgraphile/presets/amber'
import { makePgService } from 'postgraphile/adaptors/pg'
const preset = {
extends: [PostGraphileAmberPreset],
pgServices: [
makePgService({
connectionString: process.env.DATABASE_URL,
schemas: ['public'],
}),
],
plugins: [MyCustomPlugin],
}
export default presetSchema Configuration
Use type: 'schema' for custom GraphQL schemas with Grafast resolvers:
| Option | Type | Required | Description |
|--------|------|----------|-------------|
| type | 'schema' | ✅ | Configuration type discriminator |
| schema | string \| string[] \| SchemaProvider | ✅ | Path(s) to .graphql files or schema provider function |
| resolvers | string | ❌ | Path to resolvers file (required for .graphql files) |
| url | string | ❌ | GraphQL endpoint URL (default: '/graphql/') |
| graphiql | boolean | ❌ | Enable GraphiQL IDE (default: true in dev, false in prod) |
Example with files:
export default defineNuxtConfig({
grafserv: {
type: 'schema',
schema: 'server/**/*.graphql',
resolvers: 'server/resolvers.ts',
url: '/graphql',
}
})Example with schema provider function:
export default defineNuxtConfig({
grafserv: {
type: 'schema',
schema: async () => {
// Return a GraphQLSchema instance
return myCustomSchema
},
}
})Configuration Comparison
| Feature | PostGraphile Config | Schema Config |
|---------|---------------------|---------------|
| Type discriminator | type: 'postgraphile' | type: 'schema' |
| Schema source | Generated from preset | Files or function |
| Resolvers | Auto-generated by PostGraphile | Must provide via resolvers file |
| Primary use case | PostgreSQL-backed APIs | Custom GraphQL APIs |
| Setup complexity | Minimal (DB connection only) | Moderate (schema + resolvers) |
| Plugin system | PostGraphile plugins | Grafast standard steps |
Basic Usage
Schema Configuration Example
For custom schemas with resolvers:
- Create your GraphQL schema (
server/schema.graphql):
type Query {
hello: String!
ping: Boolean!
}
type Mutation {
echo(message: String!): String!
}- Create your resolvers (
server/resolvers.ts):
import { constant, lambda, type GrafastSchemaConfig } from 'grafast'
const resolvers: GrafastSchemaConfig['objects'] = {
Query: {
plans: {
hello: () => constant('world'),
ping: () => constant(true)
}
},
Mutation: {
plans: {
echo: (_source, fieldArgs) => {
const { $message } = fieldArgs
return $message
}
}
}
}
export default resolvers- Configure Nuxt:
export default defineNuxtConfig({
modules: ['@stonecrop/nuxt-grafserv'],
grafserv: {
type: 'schema',
schema: 'server/schema.graphql',
resolvers: 'server/resolvers.ts',
}
})PostGraphile Configuration Example
For database-backed GraphQL APIs:
- Create your preset file
server/graphile.preset.ts:
import { PostGraphileAmberPreset } from 'postgraphile/presets/amber'
import { makePgService } from 'postgraphile/adaptors/pg'
const preset = {
extends: [PostGraphileAmberPreset],
pgServices: [
makePgService({
connectionString: process.env.DATABASE_URL,
schemas: ['public'],
}),
],
}
export default preset- Configure PostGraphile in
nuxt.config.ts:
export default defineNuxtConfig({
modules: ['@stonecrop/nuxt-grafserv'],
grafserv: {
type: 'postgraphile',
preset: './server/graphile.preset.ts',
}
})- That's it! PostGraphile automatically generates your GraphQL schema from your PostgreSQL database.
Advanced Usage
Custom Grafserv Plugins
Add custom plugins for authentication, logging, or rate limiting through your preset file:
// server/graphile/plugins.ts
import type { GraphileConfig } from 'graphile-config'
const loggingPlugin: GraphileConfig.Plugin = {
name: 'request-logging',
version: '1.0.0',
grafserv: {
middleware: {
processGraphQLRequestBody: async (next, event) => {
console.log('Processing:', event.request.url)
return next()
}
}
}
}
const authPlugin: GraphileConfig.Plugin = {
name: 'authentication',
version: '1.0.0',
grafserv: {
middleware: {
processGraphQLRequestBody: async (next, event) => {
const token = event.request.headers.get('authorization')
// Validate token and add user to context
if (!token) {
throw new Error('Unauthorized')
}
return next()
}
}
}
}
export default [loggingPlugin, authPlugin]Import the plugins in your preset:
// server/graphile/graphile.preset.ts
import { PostGraphileAmberPreset } from 'postgraphile/presets/amber'
import { makePgService } from 'postgraphile/adaptors/pg'
import plugins from './plugins'
const preset = {
extends: [PostGraphileAmberPreset],
pgServices: [makePgService({ /* ... */ })],
plugins
}
export default presetAvailable Middleware Hooks
processRequest- Process all incoming requestsprocessGraphQLRequestBody- Process GraphQL request bodiesruruHTML- Customize Ruru IDE HTML generationonSubscribe- Handle GraphQL subscriptions
PostGraphile Integration
PostGraphile v5+ automatically generates your GraphQL schema from PostgreSQL.
With Community Plugins
import { PostGraphileAmberPreset } from 'postgraphile/presets/amber'
import { makePgService } from 'postgraphile/adaptors/pg'
import PgSimplifyInflectorPlugin from '@graphile-contrib/pg-simplify-inflector'
const preset = {
extends: [PostGraphileAmberPreset],
plugins: [PgSimplifyInflectorPlugin],
pgServices: [
makePgService({
connectionString: process.env.DATABASE_URL,
schemas: ['public'],
}),
],
schema: {
defaultBehavior: 'connection', // Enable Relay-style connections
},
grafast: {
explain: process.env.NODE_ENV === 'development', // Plan diagrams in dev
},
}
export default presetAdvanced Configuration
import { PostGraphileAmberPreset } from 'postgraphile/presets/amber'
import { makePgService } from 'postgraphile/adaptors/pg'
const preset = {
extends: [PostGraphileAmberPreset],
pgServices: [
makePgService({
connectionString: process.env.DATABASE_URL,
schemas: ['public', 'app_private'],
superuserConnectionString: process.env.SUPERUSER_DATABASE_URL, // For watch mode
pubsub: true, // Enable LISTEN/NOTIFY for subscriptions
}),
],
gather: {
// Smart tags for schema customization
pgJwtTypes: 'app_public.jwt_token',
},
schema: {
// Behavior overrides
defaultBehavior: '-insert -update -delete', // Read-only by default
pgJwtSecret: process.env.JWT_SECRET,
},
grafast: {
explain: true,
context: (requestContext) => ({
// Custom context for all resolvers
userId: requestContext.user?.id,
}),
},
}
export default presetWhy File-Based Presets?
PostGraphile presets must be in separate files, not inline in nuxt.config.ts.
The module uses PostGraphile's instance-based approach:
- At build time, the preset file is imported and a PostGraphile instance is created
- The instance is exposed via Nitro's virtual module system
- At runtime, handlers call
pgl.getSchema()to get the schema
This approach avoids GraphQL module duplication, follows PostGraphile's recommended pattern, supports watch mode, and properly handles database connections.
Your preset file needs a default export:
import { PostGraphileAmberPreset } from 'postgraphile/presets/amber'
const preset = {
extends: [PostGraphileAmberPreset],
// ...
}
export default presetImport Requirements:
Relative imports in preset files work with file extensions:
// server/graphile/graphile.preset.ts
import { PostGraphileAmberPreset } from 'postgraphile/presets/amber'
import { makePgService } from 'postgraphile/adaptors/pg'
import { MyPlugin } from './plugins/my-plugin'
import { AnotherPlugin } from './plugins/another'
const preset = {
extends: [PostGraphileAmberPreset],
pgServices: [makePgService({/*...*/})],
plugins: [MyPlugin, AnotherPlugin],
}
export default presetSchema Building with Grafast
Grafast uses a step-based execution model. Common patterns:
import { constant, lambda, access, object, filter, type GrafastSchemaConfig } from 'grafast'
const resolvers: GrafastSchemaConfig['objects'] = {
Query: {
plans: {
// Static values use constant()
hello: () => constant('world'),
// Arguments accessed via fieldArgs with $ prefix
user: (_source, fieldArgs) => {
const { $id } = fieldArgs
return lambda($id, (id) => getUserById(id))
},
// Use filter() for list filtering
userOrders: (_source, fieldArgs) => {
const { $userId } = fieldArgs
const $allOrders = constant(getAllOrders())
return filter($allOrders, $order =>
lambda([access($order, 'userId'), $userId],
([orderUserId, userId]) => orderUserId === userId
)
)
}
}
},
Mutation: {
plans: {
// Use object() to compose objects from steps
createUser: (_source, fieldArgs) => {
const { $name, $email } = fieldArgs
const $id = constant(generateId())
const $now = constant(new Date().toISOString())
const $user = object({
id: $id,
name: $name,
email: $email,
role: constant('user'),
createdAt: $now,
updatedAt: $now
})
return lambda($user, user => {
saveUser(user)
return user
})
}
}
},
// Field resolvers for types
User: {
plans: {
fullName: ($user) => {
return lambda($user, (user) => {
const typed = user as { firstName: string; lastName: string }
return `${typed.firstName} ${typed.lastName}`
})
}
}
},
// Related type resolvers
Order: {
plans: {
// Use access() to extract properties before lookups
user: ($order) => {
const $userId = access($order, 'userId')
return lambda($userId, userId => getUserById(userId as string) ?? null)
}
}
}
}
export default resolversKey Concepts:
- Resolvers return steps (constant, lambda, access, object, filter), not plain values
- Arguments are accessed via
fieldArgs.$argumentName constant()- static valueslambda()- transform step values at execution timeaccess()- extract object properties efficientlyobject()- compose objects from stepsfilter()- filter list steps
See Grafast Standard Steps for the complete reference.
Development
# Install dependencies
pnpm install
# Generate type stubs
pnpm run dev:prepare
# Develop with the playground
pnpm run dev
# Build the module
pnpm run build
# Run ESLint
pnpm run lint
# Run tests
pnpm run test
pnpm run test:watch