Container Services

This guide covers container services in AdonisJS. You will learn:

  • What container services are and how they work
  • How to use existing services in your application
  • When to use services versus dependency injection
  • How to create your own services for packages

Overview

Container services are a convenience pattern in AdonisJS that simplifies how you access framework functionality. When you need to use features like routing, hashing, or logging, you can import a ready-to-use instance instead of manually constructing classes or interacting with the IoC container directly.

This pattern exists because many framework components require dependencies that the IoC container already knows how to provide. Rather than making you resolve these dependencies yourself in every file, AdonisJS packages expose pre-configured instances as standard ES module exports. You import them like any other module, and they work immediately.

Understanding container services

Without container services, you have two options for using framework classes. You could import a class and construct it yourself, manually providing all its dependencies.

Manual construction
import { Router } from '@adonisjs/core/http'

export const router = new Router(/** Router dependencies */)

Alternatively, you could use the IoC container's make method to construct the class, letting the container handle dependency resolution.

Using app.make()
import app from '@adonisjs/core/services/app'
import { Router } from '@adonisjs/core/http'

export const router = await app.make(Router)

Container services eliminate this ceremony by doing exactly what the second approach does, but packaging it as a convenient import. The service module uses the IoC container internally and exports the resolved instance.

Using a container service
import router from '@adonisjs/core/services/router'
import hash from '@adonisjs/core/services/hash'
import logger from '@adonisjs/core/services/logger'

When you import a service, you're getting a singleton instance that was constructed by the IoC container with all its dependencies properly injected. The service itself is just a thin wrapper that makes this instance available as a standard module export.

Using container services

Container services are available automatically when you install AdonisJS packages. No configuration or registration is required. You simply import the service and use it.

Here's an example using the Drive service to upload a file to S3.

app/controllers/posts_controller.ts
import drive from '@adonisjs/drive/services/main'

export class PostsController {
  async store(post: Post, coverImage: File) {
    const coverImageName = 'random_name.jpg'

    /**
     * The drive service gives you direct access to the
     * DriveManager instance. Use it to select a disk
     * and perform file operations.
     */
    const disk = drive.use('s3')
    await disk.put(coverImageName, coverImage)
    
    post.coverImage = coverImageName
    await post.save()
  }
}

This approach is straightforward and requires no setup beyond importing the service. The Drive service is a singleton, so the same instance is shared across your entire application.

Using dependency injection instead

For applications that prefer dependency injection, you can inject the underlying class directly into your services or controllers. This approach makes your code more testable since dependencies can be easily mocked or stubbed.

Here's the same file upload functionality using constructor injection.

app/services/post_service.ts
import { Disk } from '@adonisjs/drive'
import { inject } from '@adonisjs/core'

@inject()
export class PostService {
  /**
   * The Disk instance is injected by the IoC container.
   * This makes it easy to swap implementations during
   * testing or use different disk configurations.
   */
  constructor(protected disk: Disk) {
  }

  async save(post: Post, coverImage: File) {
    const coverImageName = 'random_name.jpg'

    await this.disk.put(coverImageName, coverImage)
    
    post.coverImage = coverImageName
    await post.save()
  }
}

With dependency injection, the IoC container automatically resolves and injects the Disk instance. Your class declares what it needs, and the container provides it. This pattern is particularly valuable when writing business logic that needs to remain decoupled from framework specifics.

Available services

AdonisJS core and official packages expose the following container services. Each service corresponds to a container binding and provides access to the fully constructed class instance.

Binding Class Service
app Application @adonisjs/core/services/app
ace Kernel @adonisjs/core/services/kernel
config Config @adonisjs/core/services/config
encryption Encryption @adonisjs/core/services/encryption
emitter Emitter @adonisjs/core/services/emitter
hash HashManager @adonisjs/core/services/hash
logger LoggerManager @adonisjs/core/services/logger
repl Repl @adonisjs/core/services/repl
router Router @adonisjs/core/services/router
server Server @adonisjs/core/services/server
testUtils TestUtils @adonisjs/core/services/test_utils

Creating your own services

If you're building a package or want to expose your own container bindings as services, you can follow the same pattern that AdonisJS uses internally. A container service is simply a module that resolves a binding from the container and exports it.

You can view the complete implementation on GitHub to see how the Drive package creates its service.

Example service structure
import app from '@adonisjs/core/services/app'

let drive: DriveManager

await app.booted(async () => {
  drive = await app.container.make('drive')
})

export { drive as default }

The service waits for the application to boot, then resolves the binding from the container and exports it. This ensures all service providers have registered their bindings before the service attempts to resolve them.