State of JavaScript 2025

JavaScript Devs, We Hear You

Every year, the State of JavaScript survey asks developers what's broken. Every year, the same pain points rise to the top.

The answer isn't more tools. It's cohesion.

We've stared at lists like this for a decade. And here's what we've realized — most of these aren't tooling problems. They're cohesion problems.

The JavaScript ecosystem has world-class tools. Best-in-class ORMs. Blazing-fast routers. Type-safe validators. Flexible auth libraries. The problem is — they were never designed to work together.

It's like IKEA furniture from five different collections. Every piece is beautifully engineered. But nothing fits. You spend hours shimming, adapting, writing glue code. And the result still creaks.

Some frameworks got type safety right, but left out features. Some have features, but feel like enterprise ceremony. There are excellent auth libraries, but they need excessive plumbing.

Checkpoint 1 — Type Safety

Type safety is easy when you own the entire stack.

When one team designs the router, the validator, the ORM, the mailer, the test client — they can make sure types flow through all of them. No adapters. No type gymnastics. No as unknown as Whatever

That's the advantage of cohesion. AdonisJS doesn't just support TypeScript — it was rewritten from the ground up in 2020 with types at the core of every design decision.

Environment Variables

Access a variable that doesn't exist or misspell the key — TypeScript catches it before you run the app.

config/database.ts
const dbUrl = Env.get('DATABASE_URL')

// Typo? Immediate error.
const bad = Env.get('DATABSE_URL') // ❌
Schema validation

Define your validation schema once. The validated output is automatically typed to match — no manual interface definitions.

validators/post.ts
const schema = vine.object({
  title: vine.string(),
  tags: vine.array(vine.string())
})

const data = await request.validateUsing(schema)
Event emitter

Events are registered with their payload types. Emit an event that doesn't exist or pass the wrong shape — compile error.

services/user.ts
// Event 'user:registerd' doesn't exist
emitter.emit('user:registerd', user)

Error: Argument 'user:registerd' is not assignable to type 'user:registered' | 'user:deleted'
Redirects

Redirect to a named route. If the route doesn't exist or you pass the wrong parameters, TypeScript tells you immediately.

controllers/posts.ts
// Route 'users.shwo' doesn't exist
response.redirect().toRoute('users.shwo')

Error: Route 'users.shwo' not found. Did you mean 'users.show'?
API Tests

Test your endpoints with a typed HTTP client. Request bodies, responses, and status codes are all checked at compile time.

tests/posts.spec.ts
const response = await client.get('/api/posts/1')

response.assertStatus(200)
response.assertBodyContains({ id: 1 })
Serializers

Define how your models transform to JSON. The serialized shape becomes a type you can use on the frontend.

transformers/user.ts
const userData = userTransformer.transform(user)

// Same type available in frontend
import type { UserData } from '@api/types'
Link and Form components

Frontend components that reference your backend routes by name. Refactor a route, and every broken link surfaces as a type error.

pages/users.tsx
// Missing required param 'id'
<Link route="users.show">Profile</Link>

Error: Route 'users.show' requires params: { id: number }
API client

Generate a fully typed client from your routes. Plug it into TanStack Query, SWR, or use it directly. End-to-end type safety.

hooks/useUser.ts
const { data } = useQuery({
  queryKey: ['user', id],
  queryFn: () => api.users.get({ id })
})
Checkpoint 2 — Authentication

Authentication without the plumbing.

Authentication isn't hard. The logic is well understood — hash passwords, manage sessions, issue tokens, protect routes.

What's hard is the wiring. You pick an auth library. It needs an adapter for your ORM. The session store needs to work with your HTTP framework. The user object shape doesn't match your model. You write glue code. Then more glue code. A week passes.

In a cohesive system, this disappears.

node ace add @adonisjs/auth

# That's it. Auth is configured.
# User model? Already integrated.
# Sessions? Already working.
# Middleware? Already registered.
// Create a user
const user = await User.create({ email, password })

// Log them in (session-based)
await auth.use('web').login(user)

// That's it. Session created, cookie set.
// Verify credentials
const user = await User.verifyCredentials(email, password)

// Issue an API token
const token = await User.accessTokens.create(user)

return { token: token.value!.release() }
router
  .group(() => {
    router.get('/dashboard', [DashboardController])
    router.resource('posts', PostsController)
  })
  .use(middleware.auth())
// Redirect to GitHub
async redirect({ ally }) {
  return ally.use('github').redirect()
}

// Handle callback
async callback({ ally, auth }) {
  const gh = ally.use('github')
  const ghUser = await gh.user()
  
  const user = await User.firstOrCreate(
    { email: ghUser.email },
    { name: ghUser.name, avatar: ghUser.avatarUrl }
  )
  
  await auth.use('web').login(user)
}
Checkpoint 3 — Simplicity

Simple code that stays simple.

Structure shouldn't require ceremony. You don't need decorators on every method, module registration files, or an IoC container on your face just to wire up a controller.

AdonisJS code reads as simple as Hono or Express — but with the structure and features of a full-stack framework.

controllers/posts_controller.ts
export default class PostsController {
  async index({ auth }: HttpContext) {
    await auth.authenticate()
    return Post.all()
  }

  async store({ auth, request }: HttpContext) {
    await auth.authenticate()
    const data = await request.validateUsing(createPostValidator)
    return Post.create(data)
  }

  async show({ params }: HttpContext) {
    return Post.findOrFail(params.id)
  }
}
validators/post.ts
export const createPostValidator = vine.compile(
  vine.object({
    title: vine.string().minLength(3),
    body: vine.string().maxLength(5000).optional(),
    tags: vine.array(vine.string()),
  })
)
start/routes.ts
router
  .resource('posts', PostsController)
  .use(middleware.auth())
Checkpoint 4 — Ecosystem

The ecosystem around the framework.

A framework is only as good as what surrounds it. Documentation you can actually learn from. Tooling that doesn't fight you. A community that's been building with it for years.

Documentation

Not just API references — guides, tutorials, explanations. The 'why' alongside the 'how.'

Adocasts

Over 400 free video tutorials. From first steps to advanced patterns. A whole learning platform.

ESM-first

No dual builds. No CommonJS compatibility hacks. All-in on ES modules since 2020.

Modern tooling

HMR for your backend. CLI that scaffolds and manages. First-class VS Code extension.

Packages

Auth, ORM, validation, mail, uploads, jobs, testing — all first-party. Plus community packages.

Let's do something different today.

You've assembled the stack before. You've read the integration guides, wired the adapters, written the glue code. You know how that story ends.

This time, try cohesion.

Terminal
$ npm create adonisjs@latest my-app
Select a starter kit
Hypermedia Server-rendered with Edge templates
React Single-page app with Inertia
Vue Single-page app with Inertia
API JSON API with token auth