Skip to main content
The AdonisJS Starter Kit uses a modular architecture powered by @adonisjs-community/modules to organize code into feature-based subdirectories. This makes the codebase more maintainable, scalable, and easier to navigate.

What Are Modules?

Modules are self-contained feature directories within your AdonisJS application. Instead of organizing code by type (all controllers in one folder, all models in another), modules group related code together:
app/
├── controllers/
   ├── users_controller.ts
   ├── profile_controller.ts
   ├── auth_controller.ts
   └── posts_controller.ts
├── models/
   ├── user.ts
   └── post.ts
└── validators/
    ├── user_validator.ts
    └── auth_validator.ts
Modules make it easier to understand what code belongs together. Want to work on user management? Everything you need is in the users/ module.

Modules in the Starter Kit

The starter kit comes with several pre-built modules:
apps/web/app/
app/
├── auth/              # Authentication & authorization
   ├── controllers/   # Login, logout, register, password reset
   ├── middleware/    # Auth guards, guest middleware
   ├── mails/         # Password reset, verification emails
   ├── validators.ts  # Login, register validation schemas
   ├── routes.ts      # Auth routes
   └── ui/           # Login/register React components

├── users/             # User management
   ├── controllers/   # User CRUD, profile, impersonation
   ├── models/        # User model
   ├── policies/      # User authorization policies
   ├── services/      # User business logic
   ├── database/      # Migrations, seeders, factories
   ├── dtos/          # Data transfer objects
   ├── validators.ts  # User validation schemas
   ├── routes.ts      # User routes
   └── ui/           # User management React components

├── core/              # Core functionality
   ├── abilities/     # CASL ability definitions
   ├── exceptions/    # Custom exception handlers
   ├── middleware/    # Core middleware
   ├── policies/      # Core policies
   └── ui/           # Core UI components (layout, navigation)

├── common/            # Common utilities
   ├── controllers/   # Shared controllers
   ├── services/      # Shared services
   └── ...           # Other shared code

├── marketing/         # Marketing pages
   ├── controllers/   # Landing, about, contact
   ├── routes.ts      # Marketing routes
   └── resources/     # Marketing page React components

└── analytics/         # Analytics & tracking
    ├── controllers/   # Analytics endpoints
    └── routes.ts      # Analytics routes

Module Structure

Each module can contain any of these subdirectories:
HTTP controllers that handle requests and return responses.
users/controllers/users_controller.ts
import User from '#users/models/user'

export default class UsersController {
  async index() {
    return User.all()
  }
}
You don’t need to create all subdirectories. Only add what your module needs. For example, a simple module might only have controllers/ and routes.ts.

Import Aliases

Each module gets its own import alias defined in apps/web/package.json:
package.json
{
  "imports": {
    "#auth/*": "./app/auth/*.js",
    "#users/*": "./app/users/*.js",
    "#core/*": "./app/core/*.js",
    "#common/*": "./app/common/*.js",
    "#marketing/*": "./app/marketing/*.js",
    "#analytics/*": "./app/analytics/*.js"
  }
}

Using Import Aliases

// Import from users module
import User from '#users/models/user'
import UsersController from '#users/controllers/users_controller'
import { createUserValidator } from '#users/validators'

// Import from auth module
import { authenticate } from '#auth/middleware/auth_middleware'

// Import from core module
import { AppException } from '#core/exceptions/app_exception'
Import aliases make imports cleaner and prevent issues with relative paths like ../../models/user.

Module Registration

Modules are registered in adonisrc.ts under the preloads section:
adonisrc.ts
export default defineConfig({
  preloads: [
    () => import('#start/kernel'),

    // Marketing module
    () => import('#marketing/routes'),

    // Auth module
    () => import('#auth/start/view'),
    () => import('#auth/start/events'),
    () => import('#auth/routes'),

    // Users module
    () => import('#users/start/view'),
    () => import('#users/start/events'),
    () => import('#users/routes'),

    // Analytics module
    () => import('#analytics/routes'),
  ],
})
Preloads are executed in order. Make sure module dependencies are loaded before modules that depend on them.

Creating a New Module

Install the Package

First, ensure @adonisjs-community/modules is installed:
node ace add @adonisjs-community/modules

Generate a Module

Use the make:module command to scaffold a new module:
node ace make:module blog
This creates:
  1. A new directory: app/blog/
  2. An import alias in package.json: "#blog/*": "./app/blog/*.js"

Generate Module Files

Use the -m (or --module) flag with standard make commands:
node ace make:controller post -m=blog
# Creates: app/blog/controllers/posts_controller.ts

Create Module Routes

Create app/blog/routes.ts:
app/blog/routes.ts
import router from '@adonisjs/core/services/router'
import { middleware } from '#start/kernel'

router
  .group(() => {
    router.get('/posts', '#blog/controllers/posts_controller.index')
    router.get('/posts/:id', '#blog/controllers/posts_controller.show')
    router.post('/posts', '#blog/controllers/posts_controller.store')
    router.put('/posts/:id', '#blog/controllers/posts_controller.update')
    router.delete('/posts/:id', '#blog/controllers/posts_controller.destroy')
  })
  .prefix('/blog')
  .use(middleware.auth())

Register the Module

Add the module to adonisrc.ts:
adonisrc.ts
export default defineConfig({
  preloads: [
    // ... existing preloads
    
    // Blog module
    () => import('#blog/routes'),
  ],
})
After registering, restart your dev server for the routes to take effect.

Real Example: Users Module

Let’s look at the actual users module from the starter kit:

Directory Structure

app/users/
├── controllers/
   ├── users_controller.ts       # User CRUD operations
   ├── profile_controller.ts     # Profile management
   ├── api_tokens_controller.ts  # API token management
   └── impersonation_controller.ts  # User impersonation

├── models/
   └── user.ts                   # User model

├── policies/
   └── user_policy.ts            # User authorization

├── services/
   └── user_service.ts           # User business logic

├── database/
   ├── migrations/
   ├── create_users_table.ts
   └── create_api_tokens_table.ts
   ├── seeders/
   └── user_seeder.ts
   └── factories/
       └── user_factory.ts

├── dtos/
   └── user_dto.ts               # User DTOs

├── mails/
   └── welcome_email.ts          # Welcome email

├── ui/
   ├── pages/
   ├── index.tsx            # Users list page
   ├── show.tsx             # User detail page
   └── edit.tsx             # Edit user page
   └── components/
       ├── user-card.tsx
       └── user-form.tsx

├── start/
   ├── view.ts                  # Register view globals
   └── events.ts                # Register event listeners

├── routes.ts                    # Module routes
└── validators.ts                # Validation schemas

Import Examples

// From controllers
import User from '#users/models/user'
import { createUserValidator } from '#users/validators'
import { UserService } from '#users/services/user_service'

export default class UsersController {
  async store({ request, response }: HttpContext) {
    const data = await request.validateUsing(createUserValidator)
    const userService = new UserService()
    const user = await userService.createUser(data)
    return response.created(user)
  }
}

Routes Configuration

users/routes.ts
import router from '@adonisjs/core/services/router'
import { middleware } from '#start/kernel'

router
  .group(() => {
    // User management
    router.get('/users', '#users/controllers/users_controller.index')
    router.get('/users/:id', '#users/controllers/users_controller.show')
    router.post('/users', '#users/controllers/users_controller.store')
    router.put('/users/:id', '#users/controllers/users_controller.update')
    router.delete('/users/:id', '#users/controllers/users_controller.destroy')

    // Profile
    router.get('/profile', '#users/controllers/profile_controller.show')
    router.put('/profile', '#users/controllers/profile_controller.update')

    // API Tokens
    router.get('/tokens', '#users/controllers/api_tokens_controller.index')
    router.post('/tokens', '#users/controllers/api_tokens_controller.store')
    router.delete('/tokens/:id', '#users/controllers/api_tokens_controller.destroy')
  })
  .use(middleware.auth())

Module Communication

Modules can communicate with each other through:

1. Direct Imports

// blog/controllers/posts_controller.ts
import User from '#users/models/user'
import { authorize } from '#core/middleware/authorize_middleware'

export default class PostsController {
  async index() {
    const users = await User.all()
    return users
  }
}

2. Services

// blog/services/post_service.ts
import { UserService } from '#users/services/user_service'

export class PostService {
  async createPostForUser(userId: number, data: any) {
    const userService = new UserService()
    const user = await userService.findUser(userId)
    // Create post for user
  }
}

3. Events

// users/start/events.ts
import emitter from '@adonisjs/core/services/emitter'

emitter.on('user:created', (user) => {
  // Handle user created event
})
// blog/controllers/posts_controller.ts
import emitter from '@adonisjs/core/services/emitter'

export default class PostsController {
  async store() {
    // Create post
    emitter.emit('user:created', user)
  }
}
Prefer services and events for cross-module communication to maintain loose coupling.

Best Practices

Each module should represent a single feature or domain. Don’t create a module that does everything.Good: users/, auth/, blog/, comments/Bad: app/, main/, everything/
Code used by multiple modules belongs in common/:
// common/services/email_service.ts
export class EmailService {
  async send(to: string, subject: string, body: string) {
    // Send email
  }
}
Used in modules:
import { EmailService } from '#common/services/email_service'
Framework-level code (middleware, exceptions, policies) belongs in core/:
// core/middleware/authorize_middleware.ts
// core/exceptions/app_exception.ts
// core/policies/base_policy.ts
Keep React components organized within modules:
users/ui/
├── pages/          # Full pages (Inertia endpoints)
├── components/     # Reusable components
└── layouts/        # Module-specific layouts
If your module depends on another, document it:
// blog/README.md
# Blog Module

## Dependencies
- `#users` - For post authors
- `#core` - For authorization

Migrating Existing Code

To refactor existing code into modules:
  1. Create the module
    node ace make:module blog
    
  2. Move files
    mv app/controllers/posts_controller.ts app/blog/controllers/
    mv app/models/post.ts app/blog/models/
    
  3. Update imports
    // Change from:
    import Post from '#models/post'
    
    // To:
    import Post from '#blog/models/post'
    
  4. Extract routes
    // Move blog routes from start/routes.ts
    // To: app/blog/routes.ts
    
  5. Register module
    // adonisrc.ts
    preloads: [
      () => import('#blog/routes'),
    ]
    

Next Steps

Creating Controllers

Learn how to create controllers within modules

Database Models

Understand how to work with Lucid models in modules

Routing

Configure routes for your modules

Validation

Set up validation schemas in modules

Build docs developers (and LLMs) love