Getting Started
Want a ready-to-run template? Start with a starter: From Examples →
Prefer to understand how Axolotl works under the hood? Read on.
Why Choose Axolotl?
Axolotl eliminates the manual work and type mismatches that plague GraphQL development:
| Traditional GraphQL | Axolotl |
|---|---|
| Manual type definitions | Automatic type generation |
| Schema-code drift | Schema is the source of truth |
| Runtime type errors | Compile-time type safety |
| Resolver type casting | Zero casting needed |
| Complex setup | Simple, intuitive API |
Perfect for AI-Assisted Development
Axolotl works seamlessly with GitHub Copilot, Cursor, ChatGPT, and Claude. Clear patterns and full type safety mean AI assistants generate perfect code every time.
Learn more about AI-assisted development →
Installation
Install dependencies and the Yoga adapter:
npm i @aexol/axolotl-core @aexol/axolotl-graphql-yoga graphql-yoga graphqlOr use the quick start command:
npx @aexol/axolotl create-new my-api
cd my-api
npm install
npm run devStep 1: Define Your Schema
Create a schema.graphql file. This is your single source of truth.
type User {
_id: String!
username: String!
email: String!
createdAt: String!
}
type Post {
_id: String!
title: String!
content: String!
author: User!
createdAt: String!
}
type Query {
user(id: String!): User @resolver
posts(authorId: String, limit: Int = 10): [Post!]! @resolver
}
type Mutation {
createPost(title: String!, content: String!): Post! @resolver
}
type Subscription {
countdown(from: Int): Int @resolver
}
directive @resolver on FIELD_DEFINITION
schema {
query: Query
mutation: Mutation
subscription: Subscription
}The @resolver directive marks fields that need custom implementation.
Step 2: Generate Types
Run Axolotl's code generator:
npx @aexol/axolotl buildThis creates a models.ts file with TypeScript types for your entire schema:
// AUTO-GENERATED - DO NOT EDIT
export type Models = {
['User']: {
_id: { args: Record<string, never> };
username: { args: Record<string, never> };
email: { args: Record<string, never> };
createdAt: { args: Record<string, never> };
};
['Post']: {
_id: { args: Record<string, never> };
title: { args: Record<string, never> };
content: { args: Record<string, never> };
author: { args: Record<string, never> };
createdAt: { args: Record<string, never> };
};
['Query']: {
user: {
args: {
id: string;
};
};
posts: {
args: {
authorId?: string | undefined;
limit?: number | undefined;
};
};
};
['Mutation']: {
createPost: {
args: {
title: string;
content: string;
};
};
};
};
export interface User {
_id: string;
username: string;
email: string;
createdAt: string;
}
export interface Post {
_id: string;
title: string;
content: string;
author: User;
createdAt: string;
}
// ... more typesCreate Configuration File
On first run, Axolotl creates axolotl.json:
{
"schema": "schema.graphql",
"models": "src/models.ts"
}This config is used for all future builds. You can edit paths if needed.
Step 3: Initialize Axolotl
Create axolotl.ts to initialize the framework:
import { Models } from '@/src/models.js';
import { Axolotl } from '@aexol/axolotl-core';
import { graphqlYogaAdapter } from '@aexol/axolotl-graphql-yoga';
export const { createResolvers, createDirectives, createScalars, adapter } = Axolotl(graphqlYogaAdapter)<Models>();This exports type-safe functions for building your API.
With Custom Context (Recommended)
For real applications, you'll want custom context with database access, authentication, etc:
import { Models } from '@/src/models.js';
import { Axolotl } from '@aexol/axolotl-core';
import { graphqlYogaWithContextAdapter } from '@aexol/axolotl-graphql-yoga';
import { YogaInitialContext } from 'graphql-yoga';
import { PrismaClient } from '@prisma/client';
const db = new PrismaClient();
// Define your context type
type AppContext = YogaInitialContext & {
userId: string | null;
db: PrismaClient;
};
// Build context per request
async function buildContext(initial: YogaInitialContext): Promise<AppContext> {
const token = initial.request.headers.get('authorization');
const userId = token ? await verifyToken(token) : null;
return {
...initial, // ✅ Must spread initial context
userId,
db,
};
}
export const { createResolvers, createDirectives, createScalars, adapter } = Axolotl(
graphqlYogaWithContextAdapter<AppContext>(buildContext),
)<Models>();Step 4: Implement Resolvers
Create resolvers.ts with type-safe resolver implementations:
import { createResolvers } from '@/src/axolotl.js';
import { GraphQLError } from 'graphql';
export default createResolvers({
Query: {
user: async ([, , ctx], { id }) => {
// ✅ `id` is typed as string
// ✅ `ctx` has your custom context type
const user = await ctx.db.user.findUnique({
where: { id },
});
if (!user) {
throw new GraphQLError('User not found');
}
return user;
},
posts: async ([, , ctx], { authorId, limit = 10 }) => {
// ✅ `authorId` is typed as string | undefined
// ✅ `limit` has default value from schema
return ctx.db.post.findMany({
where: authorId ? { authorId } : undefined,
take: limit,
orderBy: { createdAt: 'desc' },
});
},
},
Mutation: {
createPost: async ([, , ctx], { title, content }) => {
// Check authentication
if (!ctx.userId) {
throw new GraphQLError('Not authenticated', {
extensions: { code: 'UNAUTHORIZED' },
});
}
// Create post
return ctx.db.post.create({
data: {
title,
content,
authorId: ctx.userId,
createdAt: new Date().toISOString(),
},
});
},
},
Post: {
// Nested resolver for author field
author: async ([source, , ctx]) => {
return ctx.db.user.findUnique({
where: { id: source.authorId },
});
},
},
Subscription: {
// Real-time countdown using createSubscriptionHandler
countdown: createSubscriptionHandler(async function* (input, { from }) {
for (let i = from || 10; i >= 0; i--) {
await new Promise((resolve) => setTimeout(resolve, 1000));
yield i;
}
}),
},
});Note: Subscription resolvers must use createSubscriptionHandler from @aexol/axolotl-core. Import it at the top:
import { createResolvers } from '@/src/axolotl.js';
import { createSubscriptionHandler } from '@aexol/axolotl-core';Understanding the Resolver Signature
(input, args) => ReturnType;inputis a tuple:[source, args, context]argsis provided as convenience (same asinput[1])
Common patterns:
// Access context only
([, , ctx]) => { ... }
// Access args via second parameter
([, , ctx], { id }) => { ... }
// Access source in nested resolvers
([source, , ctx]) => { ... }Step 5: Start the Server
Create index.ts to start your GraphQL server:
import { adapter } from '@/src/axolotl.js';
import resolvers from '@/src/resolvers.js';
const { server } = adapter(
{ resolvers },
{
yoga: {
graphiql: true, // Enable GraphiQL UI
cors: {
origin: process.env.CORS_ORIGIN || '*',
credentials: true,
},
},
},
);
const PORT = parseInt(process.env.PORT || '4000');
server.listen(PORT, () => {
console.log(`🚀 Server running at http://localhost:${PORT}`);
console.log(`📊 GraphiQL UI at http://localhost:${PORT}/graphql`);
});Run Your Server
# Development with auto-reload
npm run dev
# Production
npm run build
npm startVisit http://localhost:4000/graphql to use GraphiQL and test your API!
Project Structure
Your file tree should look like this:
my-api/
├── src/
│ ├── axolotl.ts # Framework initialization
│ ├── models.ts # Generated types (auto-generated)
│ ├── resolvers.ts # Your resolver implementations
│ └── index.ts # Server entry point
├── schema.graphql # Your GraphQL schema
├── axolotl.json # Axolotl configuration
├── package.json
└── tsconfig.jsonCommon Gotchas
1. Imports Must Use .js Extension
Axolotl uses ESM, so imports need .js extensions:
// ✅ Correct
import { createResolvers } from './axolotl.js';
// ❌ Wrong
import { createResolvers } from './axolotl';2. Regenerate Types After Schema Changes
Always run axolotl build after changing your schema:
# 1. Edit schema.graphql
# 2. Run build
npx @aexol/axolotl build
# 3. Update resolvers if needed3. Never Edit models.ts Manually
The models.ts file is auto-generated. Always edit the schema instead.
4. Context Must Extend YogaInitialContext
When using custom context:
// ✅ Correct
type AppContext = YogaInitialContext & { userId: string };
// ❌ Wrong - missing YogaInitialContext
type AppContext = { userId: string };And always spread the initial context:
// ✅ Correct
return { ...initial, userId };
// ❌ Wrong - missing spread
return { userId };Next Steps
Now that you have a working GraphQL API, explore more features:
Add Features
- Directives - Add @auth, @rateLimit, and custom logic
- Data Loaders - Prevent N+1 queries with batching
- Custom Scalars - Add DateTime, JSON, and custom types
- Testing - Write tests for your resolvers
Scale Your API
- Micro-Federation - Split schema into modules
- Best Practices - Project organization and patterns
- Recipes - Common patterns and solutions
Deploy
AI Assistance
- AI-Assisted Development - Use Cursor, Copilot, ChatGPT with Axolotl
Getting Help
- Documentation: You're here!
- Examples: Check /from-examples for complete projects
- GitHub Issues: Report bugs or request features (opens in a new tab)
- Agent Guide: See
docs/agent-guide.mdfor LLM integration tips
Comparison with Other Frameworks
vs Apollo Server
// Apollo Server - Manual type definitions
type User = {
id: string;
name: string;
};
// Axolotl - Generated from schema
// ✅ No manual types neededvs Nexus/Pothos (Code-First)
// Nexus - Define types in code
const User = objectType({
name: 'User',
definition(t) {
t.string('id');
t.string('name');
},
});
// Axolotl - Schema-first
// ✅ Schema is readable by everyone (frontend, AI, tools)
// ✅ Types generated automaticallyvs TypeGraphQL
// TypeGraphQL - Decorators and classes
@ObjectType()
class User {
@Field()
id: string;
}
// Axolotl - Simple functions
// ✅ No decorators needed
// ✅ Pure functions are easier to test
// ✅ Better with AI assistantsReady to build? Explore examples → | Read the AI guide →