JavaScript Devs, We Hear You
The same pain points. Year after year. We built AdonisJS to fix them.
We've been there too.
Express is showing its age, but that's the easy part. There are plenty of modern micro-frameworks to pick from. Choosing one gets you maybe 10% of the way there.
The other 90% is everything else. Which libraries do you pair with it? How do you structure your project? What conventions does your team follow? Where do things go when the codebase grows beyond a few files?
Every project. The same ritual. The same frustration.
The State of JS 2025 survey confirmed what we already knew: the backend ecosystem isn't missing tools. It's missing cohesion.
So we asked ourselves: what if the tools were designed to work together from day one?
TypeScript-first.
Not TypeScript-compatible.
AdonisJS made the shift to TypeScript-first in 2020. Not as a layer on top. As a rewrite with types at the core of every design decision. Every API, every return type, every configuration option is fully typed.
But we didn't stop at the framework. We thought about types at every boundary where things typically break.
Types everywhere it matters.
Env.get('DATABASE_URL')
// Returns: string
// Typo? Compile error.
Env.get('DATABASE_URL')
// Returns: string
// Typo? Compile error.
Env.get('DATABASE_URL')
// Returns: string
// Typo? Compile error.
Env.get('DATABASE_URL')
// Returns: string
// Typo? Compile error.
Env.get('DATABASE_URL')
// Returns: string
// Typo? Compile error.
Env.get('DATABASE_URL')
// Returns: string
// Typo? Compile error.
Two complaints. One root cause.
Take something as common as user signup. In most Node.js setups, this is what the process actually looks like:
- Pick an auth library.
- Read its integration guide for your specific framework.
- Configure an adapter for your specific ORM.
- Set up a session store compatible with your session middleware.
- Map the library's user object to your own model.
- Write the glue to make it all hold together.
That's before you've written a single line of product code.
$ node ace add @adonisjs/auth
It already knows your User model. It already knows your session config. It already works with your middleware.
Write your login route.
And auth is just one feature. Repeat the assembly process for validation, mail, file uploads, background jobs, testing. Each time, a new library, a new integration, a new set of compatibility questions.
We applied this thinking to every part of the stack.
All first-party. All integrated.We picked a side.
Half the npm ecosystem is CommonJS. Half is ESM. Mixing them is a special kind of hell. ERR_REQUIRE_ESM at 2am. Mysterious default import issues. Packages that work in development but break in production.
AdonisJS made the hard decision early: we're fully ESM.
The entire framework. The entire official package ecosystem. No dual builds. No compatibility layers. No "just add type: module and pray."
// No require(). No __dirname hacks. No interop headaches.
import router from '@adonisjs/core/services/router'
import { HttpContext } from '@adonisjs/core/http'
// Just... JavaScript. The way it should be in 2025.
Docs that teach, not just reference.
API references are necessary. But when you're learning a framework, you need guides. Explanations. Context. The "why", not just the "what".
AdonisJS documentation is written for humans trying to build things. Every concept explained. Every feature demonstrated. Every edge case covered.
See for yourselfOpinionated doesn't mean over-engineered.
Some frameworks mistake complexity for power. Decorators everywhere. Abstract factories. Dependency injection that requires a PhD to debug.
AdonisJS is opinionated. It gives you conventions and structure. But the code you write still looks like... code. You can read a controller and understand what it does. No magic. No ceremony.
Controllers
A resource with auth and validation.
@Controller('posts')
export class PostsController {
constructor(
@Inject(PostsService)
private postsService: PostsService,
) {}
@Get()
@UseGuards(AuthGuard)
@UseInterceptors(CacheInterceptor)
async findAll(): Promise {
return this.postsService.findAll();
}
@Post()
@UseGuards(AuthGuard)
@UsePipes(ValidationPipe)
async create(
@Body() dto: CreatePostDto
): Promise {
return this.postsService
.create(dto);
}
}
export default class PostsController {
async index({ auth }: HttpContext) {
await auth.authenticate()
const posts = await Post.all()
return posts
}
async store({ auth, request }: HttpContext) {
await auth.authenticate()
const data = await request
.validateUsing(createPostValidator)
return await Post.create(data)
}
}
// No decorators. No injected services.
// Just functions that do things.
Validation
Validating a create post request.
// npm install class-validator
// npm install class-transformer
export class CreatePostDto {
@IsString()
@IsNotEmpty()
@MinLength(3)
title: string;
@IsString()
@IsOptional()
@MaxLength(5000)
body?: string;
@IsArray()
@IsString({ each: true })
tags: string[];
}
import vine from '@vinejs/vine'
export const createPostValidator = vine
.compile(
vine.object({
title: vine.string()
.minLength(3),
body: vine.string()
.maxLength(5000)
.optional(),
tags: vine.array(
vine.string()
),
})
)
// No decorators. No classes.
// Schema in, typed data out.
Module Registration
The hidden tax. Every feature needs one.
// posts.module.ts
@Module({
imports: [
TypeOrmModule.forFeature([Post]),
AuthModule,
],
controllers: [PostsController],
providers: [
PostsService,
PostsRepository,
],
exports: [PostsService],
})
export class PostsModule {}
// app.module.ts
@Module({
imports: [
PostsModule,
UsersModule,
AuthModule,
MailModule,
// ...every feature gets one
],
})
export class AppModule {}
import router from '@adonisjs/core/services/router'
router
.resource('posts', '#controllers/posts_controller')
.middleware('auth')
// No module file.
// No provider registration.
// No exports array.
//
// Create the controller.
// Reference it in routes.
// Done.
Both frameworks are opinionated. Both are typed. Both have structure. The difference is in how much framework you have to write before you write your application.
Let's do something different today.
You've assembled the stack before. You've read the integration guides, wired the adapters, mapped the types. You know how that story ends.
Pick your frontend. Everything else is included:
- Router & Controllers
- ORM & Migrations
- Validation
- Authentication
- Sessions
- Env Management
- File Uploads
- Background Jobs
- Testing Utilities
- CORS & Security
- Edge Templating