· frameworks  · 5 min read

5 Hidden Features of AdonisJS You Didn't Know Existed

Discover five lesser-known AdonisJS features - route param/model binding, named routes & URL generation, Lucid computed/serialization tricks, advanced validator schema patterns, and streaming-safe file uploads - with practical examples and tips to make your apps faster to build and easier to maintain.

Discover five lesser-known AdonisJS features - route param/model binding, named routes & URL generation, Lucid computed/serialization tricks, advanced validator schema patterns, and streaming-safe file uploads - with practical examples and tips to make your apps faster to build and easier to maintain.

AdonisJS is an opinionated Node.js framework that packs a lot of productivity into its ecosystem. Beyond the commonly used controllers, middleware, and Lucid models, there are smaller, highly productive features that often go unnoticed - until they save you significant time.

Below are five “hidden” (or at least underused) features that can streamline development and make your code cleaner and more robust. Each section includes concise examples and practical tips so you can start using them immediately.

1) Route param binding (load models or validate params automatically)

Adonis provides a Route.param hook that runs whenever a particular route parameter is present. Use it to load and validate resources centrally instead of repeating the same lookup code in every controller action.

Example: load a User model whenever :userId is present.

// start/routes.ts
import Route from '@ioc:Adonis/Core/Route';
import User from 'App/Models/User';

Route.param('userId', async (userId, { request, response }) => {
  const user = await User.find(userId);
  if (!user) return response.status(404).send({ error: 'User not found' });

  // attach to request params so controllers receive the model
  request.params.user = user;
});

Route.get('/users/:userId', 'UsersController.show');

Then in your controller you can trust request.params.user to be a loaded User model:

// app/Controllers/Http/UsersController.ts
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';

export default class UsersController {
  public async show({ request }: HttpContextContract) {
    const user = request.params.user;
    return user;
  }
}

Why this helps

  • Centralizes lookup and 404 handling.
  • Keeps controller actions focused on business logic.

Docs: https://docs.adonisjs.com/guides/routing


2) Named routes + URL generation (stop hardcoding URLs)

Naming routes lets you generate URLs from route names rather than hardcoding them. This is great for redirects, emails, or client bundle manifest generation.

// start/routes.ts
Route.get('/users/:id', 'UsersController.show').as('users.show');

// anywhere in your server code
import Route from '@ioc:Adonis/Core/Route';

const url = Route.makeUrl('users.show', { id: 42 });
// -> '/users/42'

Practical uses

  • Generate URLs for emails or API responses.
  • Change route patterns without hunting down every reference.

Docs: https://docs.adonisjs.com/guides/routing


3) Lucid computed properties & advanced serialization

Lucid models support computed properties (derived fields) and provide hooks for fine-grained serialization control. Computed properties are ideal for composing display-ready values (e.g., fullName) without storing them in the database.

// app/Models/User.ts
import { BaseModel, column, computed } from '@ioc:Adonis/Lucid/Orm';

export default class User extends BaseModel {
  @column({ isPrimary: true })
  public id: number;

  @column() public firstName: string;
  @column() public lastName: string;

  @computed()
  public get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

// Usage
const user = await User.findOrFail(1);
console.log(user.toJSON());
// { id: 1, firstName: 'Ada', lastName: 'Lovelace', fullName: 'Ada Lovelace' }

Other serialization tips

  • You can hide sensitive columns by marking them as @column({ serializeAs: null }) or remove fields in toJSON.
  • Include or exclude relationships and extra attributes when calling serialize() or toJSON().

Docs: https://docs.adonisjs.com/guides/lucid


4) Validator power: nested schemas, arrays, and re-usable rules

Adonis’ validator is schema-first and very expressive: arrays, nested objects, and per-member rules are first-class. When you accept complex payloads (e.g., arrays of objects), use schema.array().members(...) to validate every element.

// app/Validators/CreateOrderValidator.ts
import { schema, rules } from '@ioc:Adonis/Core/Validator';

export const orderSchema = schema.create({
  customerId: schema.number([rules.exists({ table: 'users', column: 'id' })]),
  items: schema.array().members(
    schema.object().members({
      productId: schema.number([
        rules.exists({ table: 'products', column: 'id' }),
      ]),
      quantity: schema.number([rules.range(1, 100)]),
    })
  ),
  notes: schema.string.optional(),
});

// In controller
const payload = await request.validate({ schema: orderSchema });

Advanced tips

  • Reuse rule sets across validators by importing shared schemas.
  • Use schema.array.optional() and .members() to model lists that may be empty or missing.
  • Leverage built-in DB-aware rules like exists and unique for robust constraints.

Docs: https://docs.adonisjs.com/guides/validator


5) Multipart / file handling with streaming-friendly APIs

Adonis’ bodyparser supports multipart file uploads with a memory-safe, streaming approach. Use request.file() to access uploaded files and moveToDisk() (or .move()) to persist them to disk. You can also stream to external services if you prefer.

// app/Controllers/Http/AvatarsController.ts
import Application from '@ioc:Adonis/Core/Application'

public async upload({ request, response }) {
  const avatar = request.file('avatar', {
    extnames: ['jpg', 'png'],
    size: '2mb',
  })

  if (!avatar) return response.badRequest({ error: 'Avatar is required' })

  await avatar.move(Application.publicPath('uploads'), {
    name: `${new Date().getTime()}-${avatar.clientName}`,
    overwrite: false,
  })

  if (avatar.hasErrors) return avatar.errors

  return { url: `/uploads/${avatar.fileName}` }
}

Streaming to external storage

If you want to forward uploads to S3 without writing them to disk, you can access the file stream and pipe it to your storage SDK. This is ideal for large files or when running in ephemeral containers.

Why this matters

  • Avoids large memory usage by streaming files.
  • Validates files before storing them.

Docs: https://docs.adonisjs.com/guides/bodyparser


Bonus tips & best practices

  • Centralize repeated logic with route param hooks, services, or providers.
  • Name routes in large apps - it pays off when refactoring.
  • Keep validators small and composable; extract repeating schema bits to shared modules.
  • Use computed properties for formatting/derived fields, but don’t put heavy logic there (it runs whenever the model is serialized).
  • For large file uploads prefer streaming to disk or direct-to-cloud uploads to keep memory usage low.

Wrapping up

AdonisJS is full of pragmatic features that help you build well-structured, maintainable apps fast. The five techniques above - param-based route binding, named routes + URL generation, Lucid computed/serialization options, advanced validator schemas, and streaming-friendly file handling - are small changes that produce big productivity wins.

If you’d like, pick one of the examples and I can provide a drop-in implementation tailored to your app structure.

References

Back to Blog

Related Posts

View All Posts »