Getting Started

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 GraphQLAxolotl
Manual type definitionsAutomatic type generation
Schema-code driftSchema is the source of truth
Runtime type errorsCompile-time type safety
Resolver type castingZero casting needed
Complex setupSimple, 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 graphql

Or use the quick start command:

npx @aexol/axolotl create-new my-api
cd my-api
npm install
npm run dev

Step 1: Define Your Schema

Create a schema.graphql file. This is your single source of truth.

schema.graphql
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 build

This creates a models.ts file with TypeScript types for your entire schema:

src/models.ts
// 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 types

Create Configuration File

On first run, Axolotl creates axolotl.json:

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:

src/axolotl.ts
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:

src/axolotl.ts
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:

src/resolvers.ts
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;
  • input is a tuple: [source, args, context]
  • args is provided as convenience (same as input[1])

Common patterns:

// Access context only
([, , ctx]) => { ... }
 
// Access args via second parameter
([, , ctx], { id }) => { ... }
 
// Access source in nested resolvers
([source, , ctx]) => { ... }

Learn more about resolvers →


Step 5: Start the Server

Create index.ts to start your GraphQL server:

src/index.ts
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 start

Visit 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.json

Common 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 needed

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

Scale Your API

Deploy

AI Assistance


Getting Help


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 needed

vs 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 automatically

vs 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 assistants

Ready to build? Explore examples → | Read the AI guide →