Help shape what we build next. Take the AdonisJS developer survey.

The batteries-included TypeScript framework

Everything you need in one Node.js framework. Authentication, ORM, validation, mail, queues, cache, testing - all working together. Built for teams who want to ship products, not assemble frameworks.

Our sponsors

These companies keep AdonisJS independent. Their monthly support funds the ongoing work (development, maintenance, releases, and docs) for everyone who builds on it.

Become a sponsor

Code you'll actually enjoy writing

AdonisJS combines expressive APIs with clear conventions and full TypeScript support. Explore the examples below to see how common tasks stay simple without sacrificing power

import { middleware } from '#start/kernel'
import router from '@adonisjs/core/services/router'
import { controllers } from '#generated/controllers'

router
  .group(() => {
    router.get('login', [controllers.Session, 'create'])
    router.post('login', [controllers.Session, 'store'])

    router.get('signup', [controllers.NewAccount, 'create'])
    router.post('signup', [controllers.NewAccount, 'store'])
  })
  .use(middleware.guest())

// Creates a total of 7 routes to manage posts
router.resource('posts', controllers.Posts)
A functional API that centralizes your route definitions in one readable file. Know exactly what endpoints exist, how they're protected, and where they lead - all without jumping between files. Lazy-loaded controllers ensure your app stays performant as it grows
import Post from '#models/post'
import { HttpContext } from '@adonisjs/core/http'

export default class PostsController {
  async index({ request }: HttpContext) {
    const posts = await Post.query().preload('author').paginate(1, 20)
    return posts
  }

  async store({ request, auth, response }: HttpContext) {
    const data = request.validateUsing(createPostValidator)    
    const post = await auth.user.related('posts').create(data)

    return response.created(post)
  }
}
Group related request handlers in a single controller - one per resource. Each method handles the request for a specific route with full access to request data, auth, and params through HttpContext. A clear organizational pattern that scales naturally as your app grows
import vine from '@vinejs/vine'

export const createPostValidator = vine.create({
  title: vine.string().minLength(3).maxLength(255),
  content: vine.string().minLength(10),
  coverImage: vine.file({
    size: '2mb',
    extnames: ['jpg', 'png', 'webp']
  }).optional()
})

export const createUserValidator = vine.create({
  fullName: vine.string().nullable(),
  email: vine.string().email().unique({ table: 'users' }),
  password: vine.string().confirmed()
})
Twice the speed of Zod with async validation support built in. VineJS integrates deeply with AdonisJS - unique checks query your database through the ORM, file rules validate uploads seamlessly, and error messages come from your i18n files
import User from '#models/user'
import Comment from '#models/comment'
import { PostSchema } from '#database/schema'
// ...other imports

export default class Post extends PostSchema {
  @belongsTo(() => User)
  author: BelongsTo<typeof User>

  @hasMany(() => Comment)
  comments: HasMany<typeof Comment>
}

await Post.all()
await Post.query().preload('author').where('id', 1).firstOrFail()
await Post.query().withCount('comments').paginate(1, 20)
Active record ORM with a powerful query builder running on Knex. Define relationships, build complex queries with a fluent API, and manage your schema with migrations. Seeders and factories for testing and development
// ...imports
export default class AuthController {
  async login({ request, auth }: HttpContext) {
    const user = await User.verifyCredentials(email, password)
    // Create session for user
    await auth.use('web').login(user)
  }

  async loginWithToken({ request }: HttpContext) {
    const user = await User.verifyCredentials(email, password)

    // Create token for user
    const token = await User.accessTokens.create(user)
    return { token: token.value!.release() }
  }
}
Session-based authentication for web apps, access tokens for APIs, or use both in the same application. Auth middleware protects routes, password hashing happens automatically, and credential verification is built in
// ...imports
export default class AssetsController {
  async store({ request, response }: HttpContext) {
    const { file } = await request.validateUsing(createFileValidator)

    const fileName = `${uuid()}.${file.extname}`
    await file.moveToDisk(fileName, 'r2')
    
    return response.created({ fileName, url: `/assets/${fileName}` })
  }

  async show({ params, response }: HttpContext) {
    const stream = await drive.use('r2').getStream(params.fileName)
    return response.stream(stream)
  }
}
Multipart request handling with no additional packages needed. Drive provides a unified API for S3, R2, GCS, and local storage - upload and serve files with the same code regardless of provider
// ...imports
export default class WelcomeMail extends BaseMail {
  subject = 'Welcome to Acme!'
  constructor(protected user: User) {
    super()
  }

  prepare() {
    this.message
      .to(this.user.email)
      .htmlView('emails/welcome', { user: this.user })
  }
}

// Usage example
await mail.send(new WelcomeMail(user))
Support for SMTP, SES, Mailgun, SparkPost, Resend, and Brevo out of the box. Queue emails for background processing, and test your mail flow with fakes that capture messages without sending them
import limiter from '@adonisjs/limiter/services/main'

export const apiThrottle = limiter.define('api', ({ auth, request }) => {
  if (auth.user) {
    const key = `user_${auth.user.id}`
    return limiter.allowRequests(100).every('1 minute').usingKey(key)
  }

  const key = `ip_${request.ip()}`
  return limiter.allowRequests(10).every('1 minute').usingKey(key)
})

// Usage example
router
  .get('api/repos', () => {})
  .use(apiThrottle)
Redis and database storage for storing rate limiting data. Set limits dynamically based on the user, endpoint, or any request context. Atomic increments ensure accurate counting even under heavy load
import { test } from '@japa/runner'
import { UserFactory } from '#factories/user'

test.group('Posts store', (group) => {
  test('create post as authenticated user', async ({ client }) => {
    const user = await UserFactory.create()
    
    const response = await client
      .visit('posts.store')
      .loginAs(user)
      .json({ title: 'Hello World', content: 'My first post' })
    
    response.assertStatus(201)
    response.assertBodyContains(postData)
  })
})
Write browser tests using Playwright, making real HTTP calls to test API responses and easily manage database state using global transactions. Additionally, helpers like loginAs can be used to make authenticated requests

Everything you need to build

Powerful developer tooling. Official packages for common requirements. Flexible architecture that adapts to your needs. Built to take you from prototype to production.

E2E type-safety
Type-safe environment variables
Type-safe event emitter
Type-safe serialization
E2E type-safety
Type-safe environment variables
Type-safe event emitter
Type-safe serialization
Vite integration
Server-side HMR
CLI / REPL
Pretty error pages
CSRF protection
CORS support
Vite integration
Server-side HMR
CLI / REPL
Pretty error pages
CSRF protection
CORS support
Opinionated Folder structure
Dependency container
ESM
OTEL
Subpath imports
Opinionated Folder structure
Dependency container
ESM
OTEL
Subpath imports

Developer experience that just works

Pretty error pages, powerful CLI, security primitives, seamless Vite integration, and end-to-end type safety. The tooling feels invisible until you need it.

Official packages, zero hunting

From caching to rate limiting to health checks, common requirements solved with official packages. No npm rabbit holes, no compatibility roulette

View all packages

MVC with a flexible view layer

AdonisJS gives you complete MVC architecture, but the View is up to you. Pair your models and controllers with React, Vue, server-rendered templates, or build a pure API. Structured backend, flexible frontend.

Our approach towards frontend
Backend controller
import User from '#models/user'
import type { HttpContext } from '@adonisjs/core/http'
import UserTransformer from '#transformers/user_transformer'

export default class UsersController {
  async index({ request, inertia }: HttpContext) {
    const users = await User.all()
    
    return inertia.render('users/index', {
      // props serialization with type inference
      users: UserTransformer.transform(users)
    })
  }
}
import User from '#models/user'
import type { HttpContext } from '@adonisjs/core/http'
import UserTransformer from '#transformers/user_transformer'

export default class UsersController {
  async index({ request, inertia }: HttpContext) {
    const users = await User.all()
    
    return inertia.render('users/index', {
      // props serialization with type inference
      users: UserTransformer.transform(users)
    })
  }
}
import User from '#models/user'
import type { HttpContext } from '@adonisjs/core/http'

export default class UsersController {
  async index({ request, view }: HttpContext) {
    const users = await User.all()
    
    return view.render('users/index', {
      users
    })
  }
}
import User from '#models/user'
import type { HttpContext } from '@adonisjs/core/http'
import UserTransformer from '#transformers/user_transformer'

export default class UsersController {
  async index({ request, serialize }: HttpContext) {
    const users = await User.all()
    
    return serialize(UserTransformer.transform(users))
  }
}
<script setup lang="ts">
  import { Data } from '~/generated/data'
  defineProps<{ users: Data.User[] }>()
</script>

<template>
  <div v-for="user in users" :key="user.id">
    <p>{{ user.email }}</p>
  </div>
</template>
import { InertiaProps } from '~/types'
import { Data } from '~/generated/data'

type PageProps = InertiaProps<{ users: Data.User[] }>

export default function UsersIndex({ users }: PageProps) {
  return <>
    {users.map((user) => (
      <div key={user.id}>
        <p>{user.email}</p>
      </div>
    ))}
  </>
}
@each(user in users)
  <div>
    <p>{{ user.email }}</p>
  <div>
@end
{
  "data": [
    {
      "id": 1,
      "name": "John Doe",
      "email": "john@example.com",
    }
  ]
}

Releases

Changelog
Aug 08 adonisjs/inertia
Type-safe useHttp hook and Instant visits
Aug 04 adonisjs/vite
Use server.ws instead of deprecated server.hmr
Aug 04 adonisjs/inertia
Better type-safety in forms, bug fixes and protocol alignment
Jul 30 adonisjs/http-server
Allow signedURL builder to create signed URLs from a route pattern
Jul 27 adonisjs/http-server
Prevent ReDos in router's slug matcher
Jul 27 adonisjs/http-server
Backport - Prevent ReDos in router's slug matcher

Going strong for over a decade

No hype, no chasing trends. Just regular updates and commitment to building something that lasts

MIT Licensed

The framework you've been looking for

Developers are discovering what Laravel and Rails fans have known for years - batteries-included frameworks just work better

Visit the wall of love

Ready to build?

Whether you're exploring AdonisJS for the first time or ready to build your next product, here's how to get started

SCREENCASTS

Video tutorials and courses teaching AdonisJS from basics to advanced

SCREENCASTS
Visit Adocasts
AdonisJS Plus

Pre-built full-stack components and starter kits to accelerate your projects

AdonisJS Plus
Visit AdonisJS Plus
COMMUNITY

Active Discord community where developers help each other build better applications

COMMUNITY
Join Discord
DOCS

Comprehensive documentation covering every feature, written with clarity and care.

DOCS
Read documentation