Container services
As we discussed in the IoC container guide, the container bindings are one of the primary reasons for the IoC container to exists in AdonisJS.
Container bindings keep your codebase clean from boilerplate code required to construct objects before they can be used.
In the following example before you can use the Database
class, you will have to create an instance of it. Depending the class you are constructing, you might have write a lot of boilerplate code to get all of its dependencies.
import { Database } from '@adonisjs/lucid'
export const db = new Database(
// inject config and other dependencies
)
However, when using an IoC container, you can offload the task of constructing a class to the container and fetch a pre-built instance.
import app from '@adonisjs/core/services/app'
const db = await app.container.make('lucid.db')
The need for container services
Using the container to resolve pre-configured objects is great. However, using the container.make
method has its own downsides.
-
Editors are good with auto imports. If you attempt to use a variable and the editor can guess the import path of the variable, then it will write the import statement for you. However, this cannot work with
container.make
calls. -
Using a mix of import statements and
container.make
calls feels unintuitive compared to having a unified syntax for importing/using modules.
To overcome these downsides, we wrap container.make
calls inside a regular JavaScript module, so you can fetch them using the import
statement.
For example, the @adonisjs/lucid
package has a file called services/db.ts
and this file has roughly the following contents.
const db = await app.container.make('lucid.db')
export { db as default }
Within your application, you can replace the container.make
call with an import pointing to the services/db.ts
file.
import app from '@adonisjs/core/services/app'
const db = await app.container.make('lucid.db')
import db from '@adonisjs/lucid/services/db'
As you can see, we are still relying on the container to resolve an instance of the Database class for us. However, with a layer of indirection, we can replace the container.make
call with a regular import
statement.
The JavaScript module wrapping the container.make
calls is known as a Container service. Almost every package that interacts with the container ships with one or more container services.
Container services vs. Dependency injection
Container services are an alternative to dependency injection. For example, instead of accepting the Disk
class as a dependency, you ask the drive
service to give you a disk instance. Let's look at some code examples.
In the following example, we use the @inject
decorator to inject an instance of the Disk
class.
import { Disk } from '@adonisjs/drive'
import { inject } from '@adonisjs/core'
@inject()
export class PostService {
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()
}
}
When using the drive
service, we call the drive.use
method to get an instance of Disk
with s3
driver.
import drive from '@adonisjs/drive/services/main'
export class PostService {
async save(post: Post, coverImage: File) {
const coverImageName = 'random_name.jpg'
const disk = drive.use('s3')
await disk.put(coverImageName, coverImage)
post.coverImage = coverImageName
await post.save()
}
}
Container services are great for keeping your code terse. Whereas, dependency injection creates a loose coupling between different application parts.
Choosing one over the other comes down to your personal choice and the approach you want to take to structure your code.
Testing with container services
The outright benefit of dependency injection is the ability to swap dependencies at the time of writing tests.
To provide a similar testing experience with container services, AdonisJS provides first-class APIs for faking implementations when writing tests.
In the following example, we call the drive.fake
method to swap drive disks with an in-memory driver. After a fake is created, any call to the drive.use
method will receive the fake implementation.
import drive from '@adonisjs/drive/services/main'
import { PostService } from '#services/post_service'
test('save post', async ({ assert }) => {
/**
* Fake s3 disk
*/
drive.fake('s3')
const postService = new PostService()
await postService.save(post, coverImage)
/**
* Write assertions
*/
assert.isTrue(await drive.use('s3').exists(coverImage.name))
/**
* Restore fake
*/
drive.restore('s3')
})
Container bindings and services
The following table outlines a list of container bindings and their related services exported by the framework core and first-party packages.
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
|