A zero-runtime GraphQL query generation system that brings PandaCSS's approach to GraphQL. Write type-safe queries in TypeScript that are statically analyzed and transformed at build time into optimized GraphQL documents.
- 🔍 Full Type Safety: Complete TypeScript inference from schema to query results
- 🎯 No Code Generation Loop: Unlike traditional GraphQL codegen, no constant regeneration needed
- 💡 LSP Integration: VS Code extension with hover, completion, and diagnostics for tagged templates
- 🔧 Transform Functions: Built-in data normalization at the fragment level
- 📦 Modular Architecture: Compose queries from reusable fragments
- ⚡ Instant Feedback: Type errors appear immediately in your IDE
packages/
├── core/ # Core GraphQL types, utilities, and runtime
├── dev/ # CLI, codegen, typegen, and formatter (dev tooling)
├── builder/ # Static analysis & artifact generation
├── babel/ # Babel transformer and plugin
├── tsc/ # TypeScript transformer and plugin
├── swc/ # SWC-based native transformer
├── webpack-plugin/ # Webpack plugin with HMR support
├── vite-plugin/ # Vite plugin
├── metro-plugin/ # Metro plugin (React Native)
├── lsp/ # Language Server Protocol implementation
└── vscode-extension/ # VS Code extension for LSP
# Install packages
bun add @soda-gql/core
bun add -D @soda-gql/tools @soda-gql/configCreate a soda-gql.config.ts file in your project root:
import { defineConfig } from "@soda-gql/config";
export default defineConfig({
outdir: "./src/graphql-system",
include: ["./src/**/*.ts"],
schemas: {
default: {
schema: "./schema.graphql",
inject: "./src/graphql-system/default.inject.ts",
},
},
});Generate the GraphQL system:
# Scaffold inject template with scalar and adapter definitions (first-time setup)
bun run soda-gql codegen schema --emit-inject-template ./src/graphql-system/default.inject.ts
# Generate GraphQL system from schema
bun run soda-gql codegen schemaThe generated module imports your scalar definitions from the inject file. Keep the inject file (e.g., default.inject.ts) under version control so custom scalar behavior stays explicit.
Recommended workflow: Install the VS Code extension to get real-time type information, completion, and diagnostics without running typegen.
# Install the VS Code extension
code --install-extension soda-gql-*.vsixWith the extension installed, you get:
- Hover type information: See field types directly in tagged templates
- Inlay hints: Display GraphQL return types inline (
: String!,: [Post!]!) - Field completion: Auto-complete based on your schema
- Diagnostics: Real-time error detection for invalid fields
- Fragment interpolation support: Use
...${fragment}spreads with full LSP support
This enables a development without typegen workflow:
- Define
schema.graphql - Run
codegen schema(one-time setup) - Install VS Code extension
- Write tagged templates with full LSP support
- Optionally run
typegenfor compile-time type safety
See LSP Workflow Guide for detailed setup and usage.
| File | Purpose | Version Control |
|---|---|---|
{schema}.inject.ts |
Custom scalar TypeScript types (hand-edit) | ✅ Commit |
index.ts |
Generated schema types and gql composer | ❌ .gitignore |
index.js, index.cjs |
Bundled output (by tsdown) | ❌ .gitignore |
Note: The inject file defines TypeScript types for custom scalars (DateTime, JSON, etc.). Scaffold it once with --emit-inject-template, then customize as needed. The generated index.ts and bundled outputs should be added to .gitignore.
Already have .graphql operation files? Generate soda-gql compat code:
bun run soda-gql codegen graphql --input "src/**/*.graphql"This creates TypeScript files using the compat pattern, preserving your existing operations while gaining full type safety.
soda-gql supports two syntax styles: tagged templates (recommended for most cases) and callback builders (for advanced features).
Write GraphQL directly — fragments and operations use familiar GraphQL syntax:
import { gql } from "@/graphql-system";
// Define a reusable fragment
export const userFragment = gql.default(({ fragment }) =>
fragment("UserFragment", "User")`($categoryId: ID) {
id
name
posts(categoryId: $categoryId) {
id
title
}
}`(),
);
// Build a complete operation
export const listUsersQuery = gql.default(({ query }) =>
query("ListUsers")`($categoryId: ID) {
users {
id
name
posts(categoryId: $categoryId) {
id
title
}
}
}`(),
);Use callback builders when you need features like field directives (@skip, @include), $colocate, or $var:
import { gql } from "@/graphql-system";
// Fragment with field alias and $var (callback builder for $var)
export const userFragment = gql.default(({ fragment, $var }) =>
fragment.User({
variables: { ...$var("categoryId").ID("?") },
fields: ({ f, $ }) => ({
...f.id(null, { alias: "uuid" }),
...f.name(),
...f.posts({ categoryId: $.categoryId })(({ f }) => ({
...f.id(),
...f.title(),
})),
}),
}),
);
// Operation with fragment spread and $var (callback builder for $var)
export const profileQuery = gql.default(({ query, $var }) =>
query.operation({
name: "ProfileQuery",
variables: { ...$var("userId").ID("!"), ...$var("categoryId").ID("?") },
fields: ({ f, $ }) => ({
...f.users({
id: [$.userId],
categoryId: $.categoryId,
})(({ f }) => ({ ...userFragment.spread({ categoryId: $.categoryId }) })),
}),
}),
);When to use each syntax:
| Feature | Tagged Template | Callback Builder |
|---|---|---|
| Simple field selections | Yes | Yes |
| Variables and arguments | Yes | Yes |
| Nested object selections | Yes | Yes |
| Fragment spreads in fragments | Yes (${otherFragment}) |
Yes (.spread()) |
| Fragment spreads in operations | Yes (...${frag}) |
Yes (.spread()) |
| Field aliases | Yes | Yes |
| Metadata callbacks | Yes | Yes |
$colocate |
— | Yes |
| Document transforms | — | Yes |
Attach runtime information to operations for HTTP headers and application-specific values:
// Operation with metadata
export const userQuery = gql.default(({ query, $var }) =>
query.operation({
name: "GetUser",
variables: { ...$var("userId").ID("!") },
metadata: ({ $ }) => ({
headers: { "X-Request-ID": "user-query" },
custom: { requiresAuth: true, cacheTtl: 300 },
}),
fields: ({ f, $ }) => ({ ...f.user({ id: $.userId })(({ f }) => ({ ...f.id(), ...f.name() })) }),
}),
);See @soda-gql/core README for detailed documentation on metadata structure and advanced usage.
Use define to share configuration values and helper functions across multiple gql definitions:
// shared/config.ts
export const ApiConfig = gql.default(({ define }) =>
define(() => ({
defaultTimeout: 5000,
retryCount: 3,
}))
);
// queries/user.ts
import { ApiConfig } from "../shared/config";
export const GetUser = gql.default(({ query, $var }) =>
query.operation({
name: "GetUser",
variables: { ...$var("id").ID("!") },
metadata: () => ({
custom: { timeout: ApiConfig.value.defaultTimeout },
}),
fields: ({ f, $ }) => ({ ... }),
})
);Values defined with define pass builder evaluation but are excluded from the final artifact.
See Define Element Guide for detailed documentation.
When bundling with tools like tsdown, rollup-plugin-dts, or other bundlers that merge declaration files, complex type inference (like InferFields) may be lost at module boundaries. The prebuilt types feature solves this by pre-calculating types at build time.
Generate the prebuilt type registry alongside the regular output:
# First generate the GraphQL system
bun run soda-gql codegen schema
# Then generate prebuilt types
bun run soda-gql typegenNote:
@swc/coreis required for template extraction during typegen. Install it as a dev dependency:bun add -D @swc/core
This produces the following output structure:
{config.outdir}/
├── _internal.ts # Schema composers and internal definitions
├── index.ts # Main module with prebuilt type resolution
└── types.prebuilt.ts # Pre-calculated type registry (populated by typegen)
The index.ts module automatically references the types.prebuilt.ts registry — no path aliases or import changes needed.
Fragments require a key property to be included in prebuilt types. Fragments without keys work at runtime but are silently skipped during prebuilt type generation:
// Fragment with key for prebuilt type lookup
export const userFragment = gql.default(({ fragment }) =>
fragment.User("UserFields")`
id
name
`(),
);Operations are automatically keyed by their name property.
For detailed documentation, see Prebuilt Types Guide.
# Install dependencies
bun install
# Run tests
bun test
# Type check all packages
bun typecheck
# Run quality checks (Biome + TypeScript)
bun qualityThis is a monorepo using Bun workspaces. Each package is independently versioned and can be developed in isolation.
bun quality- Run Biome linting/formatting and TypeScript checksbun typecheck- Type check all packagesbun biome:check- Run Biome with auto-fix
We follow TDD (Test-Driven Development) with the t_wada methodology:
- Write test first (RED phase)
- Make it pass (GREEN phase)
- Refactor (REFACTOR phase)
- TypeScript: Strict mode enabled, no
anytypes - Error Handling: Using
neverthrowfor type-safe Results - Validation: Using
zodv4 for external data validation - Formatting: Biome v2 with automatic import sorting
MIT