Architecture
Understanding how Axolotl works under the hood will help you build better GraphQL APIs. This guide covers the core concepts and architecture decisions.
Overview
Axolotl follows a schema-first approach where your GraphQL schema is the single source of truth. Everything else—types, resolvers, and runtime—is generated or configured based on that schema.
┌─────────────────┐
│ schema.graphql │ ← Single source of truth
└────────┬────────┘
│
↓
┌────────────────────┐
│ axolotl build │ ← Code generation
└────────┬───────────┘
│
↓
┌────────────────────┐
│ src/models.ts │ ← Generated TypeScript types
└────────┬───────────┘
│
↓
┌────────────────────┐
│ src/axolotl.ts │ ← Framework initialization
└────────┬───────────┘
│
↓
┌────────────────────┐
│ src/resolvers.ts │ ← Type-safe resolvers
└────────┬───────────┘
│
↓
┌────────────────────┐
│ GraphQL Server │ ← Runtime (Yoga, etc.)
└────────────────────┘Core Components
1. Schema (schema.graphql)
Your GraphQL schema defines your API contract:
type User {
id: ID!
name: String!
posts: [Post!]!
}
type Query {
user(id: ID!): User @resolver
}Why schema-first?
- Clear API contract for frontend and backend
- AI tools understand schemas better than code
- Easy to generate documentation
- Client types can be generated from the same schema
- Schema evolution is explicit and trackable
2. Code Generator (axolotl build)
The CLI reads your schema and generates TypeScript types:
npx @aexol/axolotl buildWhat it generates:
- Type definitions for all GraphQL types
- Argument types for fields
- Return types for resolvers
- Directive types
- Scalar mappings
Generated code example:
export type Models = {
['Query']: {
user: {
args: {
id: string;
};
};
};
};3. Framework Core (axolotl-core)
The core package provides:
- Type-safe resolver creation
- Directive system
- Scalar handling
- Adapter interface
Key exports:
Axolotl()- Main initialization functioncreateResolvers()- Type-safe resolver buildercreateDirectives()- Directive buildercreateScalars()- Scalar buildermergeAxolotls()- Combine multiple resolver sets
4. Adapters
Adapters connect Axolotl to GraphQL servers:
import { graphqlYogaAdapter } from '@aexol/axolotl-graphql-yoga';
export const { adapter, createResolvers } = Axolotl(graphqlYogaAdapter)<Models>();Current adapters:
@aexol/axolotl-graphql-yoga- Node.js (GraphQL Yoga)@aexol/axolotl-deno-yoga- Deno (GraphQL Yoga)
Adapter responsibilities:
- Map framework requests to Axolotl resolver signature
- Handle context building
- Configure server options
- Start/stop server
How Type Safety Works
Schema to TypeScript
When you define a field in GraphQL:
type Query {
user(id: ID!, includeDeleted: Boolean): User
}Axolotl generates:
type Models = {
['Query']: {
user: {
args: {
id: string; // ID! → string
includeDeleted?: boolean; // Boolean → boolean | undefined
};
};
};
};Resolver Type Inference
When you create resolvers:
createResolvers({
Query: {
user: async ([, , ctx], args) => {
// TypeScript knows:
// args.id is string
// args.includeDeleted is boolean | undefined
// Return type must match User from schema
},
},
});The createResolvers function is generic and infers types from your Models type.
Context Typing
Custom context is typed through the adapter:
type AppContext = YogaInitialContext & {
userId: string | null;
db: PrismaClient;
};
const { createResolvers } = Axolotl(graphqlYogaWithContextAdapter<AppContext>(buildContext))<Models>();
// Now all resolvers have typed context:
createResolvers({
Query: {
me: ([, , ctx]) => {
// ctx.userId: string | null ✅
// ctx.db: PrismaClient ✅
},
},
});The Resolver Signature
Axolotl uses a unique resolver signature:
(input, args) => ReturnType;Where input is [source, args, context].
Why this signature?
- Consistency - Same pattern everywhere
- Type safety - Tuple types work perfectly with TypeScript
- AI-friendly - Easy for AI to learn and replicate
- Flexibility - Destructure what you need
Comparison with standard GraphQL:
// Standard GraphQL
(parent, args, context, info) => result
// Axolotl
(input, args) => result
// where input = [parent, args, context]
// info is rarely needed, so we omit it by defaultConfiguration System
axolotl.json
The config file stores paths and options:
{
"schema": "schema.graphql",
"models": "src/models.ts",
"federation": [
{
"schema": "src/users/schema.graphql",
"models": "src/users/models.ts"
}
]
}Config flow:
- CLI reads
axolotl.json - Finds all schema files
- Generates types for each
- Merges federated schemas if configured
Runtime Configuration
Server options are passed to the adapter:
adapter(
{ resolvers, directives, scalars },
{
yoga: {
graphiql: true,
cors: { origin: '*' },
maskedErrors: false,
},
},
);Micro-Federation Architecture
Federation allows splitting one schema into multiple modules:
Project
├── schema.graphql (merged supergraph)
├── src/
│ ├── users/
│ │ ├── schema.graphql (users subgraph)
│ │ ├── models.ts (generated)
│ │ ├── axolotl.ts (user module init)
│ │ └── resolvers.ts
│ └── posts/
│ ├── schema.graphql (posts subgraph)
│ ├── models.ts (generated)
│ ├── axolotl.ts (posts module init)
│ └── resolvers.tsBenefits:
- Team ownership of modules
- Independent type generation
- Clear boundaries
- Easy to extract to microservices later
How it works:
- Each module has its own schema and types
- Each module exports resolvers
- Main file merges all resolvers with
mergeAxolotls() - CLI merges all schemas into supergraph
See Micro-Federation for details.
Adapter System
Adapters are the bridge between Axolotl and GraphQL servers.
Adapter Interface
interface AxolotlAdapter {
// Convert framework types to Axolotl types
resolver: (fn: AxolotlResolver) => FrameworkResolver;
// Initialize server
initialize: (config: Config) => Server;
}How Adapters Work
- Receive framework request (e.g., from GraphQL Yoga)
- Extract source, args, context
- Convert to tuple
[source, args, context] - Call Axolotl resolver with tuple
- Return result to framework
Writing Custom Adapters
You can write adapters for any GraphQL server:
import { AxolotlAdapter } from '@aexol/axolotl-core';
export const myFrameworkAdapter: AxolotlAdapter = {
resolver: (axolotlResolver) => {
return (parent, args, context, info) => {
// Convert to Axolotl signature
return axolotlResolver([parent, args, context], args);
};
},
initialize: (config) => {
// Set up your GraphQL server
return createServer(config);
},
};Data Flow
Request Flow
1. GraphQL Request
↓
2. Server (GraphQL Yoga)
↓
3. Adapter (converts signature)
↓
4. Axolotl Resolver
↓
5. Your Code (business logic)
↓
6. Database/External API
↓
7. Return Response
↓
8. Type Validation
↓
9. GraphQL ResponseBuild-Time Flow
1. Edit schema.graphql
↓
2. Run: axolotl build
↓
3. CLI reads schema
↓
4. Generate TypeScript types
↓
5. Write to models.ts
↓
6. TypeScript compiler picks up changes
↓
7. Your IDE updates autocompletePerformance Considerations
Type Generation
- Fast: Happens at build time, not runtime
- Cached: Only regenerates when schema changes
- Minimal: Only generates types, not runtime code
Runtime
- Zero overhead: Types are erased at runtime
- Direct function calls: No proxies or wrappers
- Efficient: Resolvers are plain JavaScript functions
Optimization Tips
- Use DataLoaders for N+1 queries
- Enable caching with directives
- Batch database queries in resolvers
- Use field-level resolvers only when needed
- Profile with GraphQL tools to find bottlenecks
Design Decisions
Why Schema-First?
Pros:
- ✅ Clear contract for all stakeholders
- ✅ Better for AI assistance
- ✅ Easy to generate client types
- ✅ Schema is self-documenting
- ✅ Frontend can work in parallel
Cons:
- ❌ Extra step to update schema
- ❌ Can't generate schema from code
- ❌ Two files to keep in sync (schema + resolvers)
Our take: The pros heavily outweigh the cons, especially with AI tools that can help keep schema and code in sync.
Why Tuple Input?
Standard GraphQL:
(parent, args, context, info) => result;Axolotl:
([parent, args, context], args) => result;Reasons:
- Consistency - Same pattern everywhere
- Flexibility - Destructure what you need
- Type safety - Tuples work great with TypeScript
- AI-friendly - Simpler for AI to learn
Why Adapters?
Adapters allow Axolotl to work with any GraphQL server:
- Flexibility: Choose your server
- Future-proof: Add new servers without breaking changes
- Testability: Mock adapters for testing
- Simplicity: Core stays clean and focused
Comparison with Other Frameworks
vs Code-First (Nexus, Pothos, TypeGraphQL)
Axolotl (Schema-First):
type User {
id: ID!
name: String!
}- ✅ Readable by everyone (including AI)
- ✅ Can generate client types
- ✅ Clear API contract
- ✅ Self-documenting
Code-First:
const User = objectType({
/* ... */
});- ❌ Only developers can read
- ❌ Harder for AI to understand
- ❌ Schema hidden in code
- ❌ Need decorators or builders
vs Apollo Server
Axolotl:
- ✅ Automatic type generation
- ✅ Zero manual type definitions
- ✅ Compile-time type safety
Apollo Server:
- ❌ Manual type definitions
- ❌ Runtime type checking only
- ❌ Easy to have type drift
Next Steps
- Learn about Best Practices for production apps
- Explore Recipes for common patterns
- Check out Micro-Federation for scaling
- Read the AI Guide for AI-assisted development