Why Use A Service Provider?
Service Providers are a great place to turn when you need to bootstrap, or register, something inside of your AdonisJS application. Service providers can be used to register event listeners, middleware, routes, and more.
They’re also great when you need to extend AdonisJS as well. For example, back in the routing module of this series, we used a RouteProvider
to go over how we can extend the Route module within AdonisJS.
We can also use Service Providers to register a package from outside the AdonisJS ecosystem so that it’s available within the AdonisJS ecosystem. You’ll only want to use Service Providers for this if the package requires some setup work. Packages like gravatar, and others that can be imported and immediately used, don’t require a Service Provider.
What Is A Service Provider
The reason there are so many different reasons service providers can be used is because, at the core, they’re classes that contain AdonisJS lifecycle hook methods. Meaning, AdonisJS will import each registered provider and assign the applicable lifecycle hook method to run when your AdonisJS application reaches that lifecycle hook.
When you create a new provider using the Ace CLI, below is the file that’ll be created.
import { ApplicationContract } from '@ioc:Adonis/Core/Application' /* |-------------------------------------------------------------------------- | Provider |-------------------------------------------------------------------------- | | Your application is not ready when this file is loaded by the framework. | Hence, the top level imports relying on the IoC container will not work. | You must import them inside the life-cycle methods defined inside | the provider class. | | @example: | | public async ready () { | const Database = this.app.container.resolveBinding('Adonis/Lucid/Database') | const Event = this.app.container.resolveBinding('Adonis/Core/Event') | Event.on('db:query', Database.prettyPrint) | } | */ export default class ExampleProvider { constructor(protected app: ApplicationContract) {} public register() { // Register your own bindings } public async boot() { // All bindings are ready, feel free to use them } public async ready() { // App is ready } public async shutdown() { // Cleanup, since app is going down } }
Copied!
As you can see there are four lifecycle hook methods populated on the provider class.
register Called before your application is booted and before namespace bindings have been registered. This is where you can register your own namespace bindings within your application.
boot Called when your application has booted. At this point and time, all namespace bindings within your application have been registered and are ready to use.
ready Called when your application is fully running. Think of this as the equivalent of client-side JavaScript’s DOMContentLoaded event.
shutdown Called when your application is being shut down. This is where you’ll want to clean up anything you’ve registered that needs to be destroyed in order for your application to gracefully shut down.
Top-Level Imports
An important thing to note, as commented in the provider code above, is that when the provider file is imported, the IoC Container is not yet ready. So, in order to import files that use the IoC Container, you’ll need to import them within the lifecycle method you need to use them within.
As the comment example outlines, an example of this is:
public async ready () { const Database = this.app.container.resolveBinding('Adonis/Lucid/Database') const Event = this.app.container.resolveBinding('Adonis/Core/Event') Event.on('db:query', Database.prettyPrint) }
Copied!
Here, we’re importing the Database and Event modules within the ready
lifecycle method inside of a provider.
Inside of these lifecycle hooks, we can make use of the IoC Container via this.app.container
. So, in the example, we’re using the IoC Container to resolve a registered namespace binding for both the Database and Event modules.
Creating A Service Provider
When it comes to creating a Service Provider, you could manually create and register a file equivalent to the above within your application, however, you can also use the Ace CLI to easily generate one into your project for you.
The Ace CLI command we’ll want to run to generate a new provider file is:
node ace make:provider <name>
Copied!
Let’s take a look at the Ace CLI help options for this command.
node ace make:provider -h Make a new provider class Usage: make:provider <name> Arguments name Name of the provider class Flags --ace boolean Register provider under the ace providers array -e, --exact boolean Create the provider with the exact name as provided
Copied!
Here we can see the command expects a name, which should be the name of the provider class generated. We also have two flags.
--ace, which we can use to specify whether we want the provider to be registered under the providers array inside our .adonisrc.json file for us. This will default to true when excluded.
-e, which you can use to bypass AdonisJS’ naming normalizations.
Using A Service Provider to Register a Third-Party Package
Now that we have an idea of what providers are and what they can be used for, let’s walk through an example of using a Service Provider to wrap a NodeJS package and register it within our application.
Installing The Package
For this example, we’ll be wrapping the node-discord-logger package for easy use inside of our AdonisJS application. However, before we can use the package we must first install it!
npm i node-discord-logger
Copied!
Creating The Provider
Next, let’s go ahead and create the provider file. The below command will also register the provider file inside our .adonisrc.json
file as well.
node ace make:provider LogProvider
Copied!
This will create our LogProvider file within our app’s providers directory. However, in order to leave space for future loggers to be configured within our application, let’s actually move this into a directory called LogProvider
and rename the file to index.ts
so it’s the default import. Our updated file path should now be:
/providers/LogProvider/index.ts
Wrapping The Package
This step is optional, you could just use the package as-is. However, for demonstration purposes and to provide a slightly more lenient API, let’s go over how to wrap the package.
So, firstly let’s create a discord.ts
file within our /providers/LogProvider
directory. Let’s also define a default exported class called Logger
here as well with an instance of the node-discord-logger
package on it.
import DiscordLogger from 'node-discord-logger' export default class Logger { protected logger: DiscordLogger constructor() { this.logger = new DiscordLogger() } }
Copied!
- providers
- LogProvider
- discord.ts
Config
The node-discord-logger
has a number of configuration options as well. So, it’d be great to accept a configuration into the constructor to pass into our node-discord-logger
instance. Let’s create a new file within our config
directory called log.ts
. This config file can house all configurations for any future loggers we may add to the project.
For now, let’s use the following.
import Env from '@ioc:Adonis/Core/Env' export const discordLoggerConfig = { hook: Env.get('DISCORD_WEBHOOK'), serviceName: Env.get('NODE_ENV') }
Copied!
- config
- log.ts
The node-discord-logger
package accepts a lot more config options, but let’s keep it simple to a hook
and serviceName
for now. Both of which, we’ll load from our environment variables.
hook should be the webhook URL. You’ll need to go and grab one from Discord if you’re following along.
serviceName is text displayed in the footer of Discord messages. In our case, we’ll use the NODE_ENV to display whether the message was sent in production, development, or testing.
Applying The Configuring
Now that we’ve got our package’s configuration setup, let’s put it to use in our Logger class.
import DiscordLogger from 'node-discord-logger' import { discordLoggerConfig } from 'Config/log' export default class Logger { protected logger: DiscordLogger constructor(config: typeof discordLoggerConfig) { this.logger = new DiscordLogger(config) } }
Copied!
- providers
- LogProvider
- discord.ts
Here we’re accepting the configuration as the class constructor and passing it into the DiscordLogger
constructor.
Adding Methods
Lastly, let’s add some methods to our wrapper.
import DiscordLogger from 'node-discord-logger' import { discordLoggerConfig } from 'Config/log' export default class Logger { protected logger: DiscordLogger constructor(config: typeof discordLoggerConfig) { this.logger = new DiscordLogger(config) } public async info(title: string, message?: string | object| Array<any>) { return this.logger.info(this.build(title, message)) } public async warn(title: string, message?: string | object| Array<any>) { return this.logger.warn(this.build(title, message)) } public async error(title: string, message?: string | object| Array<any>) { return this.logger.error(this.build(title, message)) } public async debug(title: string, message?: string | object| Array<any>) { return this.logger.debug(this.build(title, message)) } public async silly(title: string, message?: string | object| Array<any>) { return this.logger.silly(this.build(title, message)) } private build(title: string, message?: string | object | Array<any>) { return { message: title, description: JSON.stringify(message, null, 4) } } }
Copied!
- providers
- LogProvider
- discord.ts
Here we have an info, warn, error, debug, and silly method that all accept the same arguments. Then, we have a build method that all the other methods call, that returns back an object in the format DiscordLogger
expects. We’ll also stringify the description message so we can provide a string, object, or array to it.
Registering The Package Wrapper
Next, let’s register our Logger
class to the Logger/Discord
namespace within our application. Once we do this, and inform TypeScript about it, we’ll be able to import our package via @ioc:Logger/Discord
.
Let’s focus on the register
method of our LogProvider
class. First, we’ll want to use the IoC Container to register our namespace as a singleton.
export default class LogProvider { constructor (protected app: ApplicationContract) { } public register () { // Register your own bindings this.app.container.singleton('Logger/Discord', () => { // 1. import the discord logger configuration const { discordLoggerConfig } = this.app.config.get('log') // 2. import our Logger wrapper class const DiscordLogger = require('./discord').default // 3. return a new instance return new DiscordLogger(discordLoggerConfig) }) } // ... }
Copied!
- providers
- LogProvider
- index.ts
Here we’re using the IoC Container to register a singleton to the namespace Logger/Discord
. Since we’re using the IoC Container container
property here, we don’t want to specify the @ioc:
prefix on the namespace.
The second argument is where we’ll create our Logger class instance and return it back as the value for our Logger/Discord
namespace.
Inside that callback, we’re using the app
config property to call get. This allows us to easily get a configuration file within our provider’s lifecycle methods. Here all we need to provide is the file’s name, log
. Then, we import our Logger
class, calling it DiscordLogger
. Lastly, we return back a new instance of the DiscordLogger
class, passing it our configuration.
Singleton VS Bind
Now, a small aside. Above we defined our namespace as a singleton. However, we could define it as a normal binding as well, by replacing singleton
with bind
.
this.app.container.bind('Logger/Discord', () => { })
Copied!
You may be asking, what’s the difference? We’ll bind will call the callback function every time we import our Logger/Discord
namespace. Singleton, however, will only call the callback function once throughout the lifetime of our server.
So, with bind, we’d end up creating multiple Logger
instances, one each time we import the namespace. With singleton, we’ll only be creating one, regardless of how many times we import the namespace.
Informing TypeScript About the Binding
The last thing to do before we can use it is to inform TypeScript about the new namespace binding! Again, to allow the possibility for additional loggers to be added in the future, let’s add a logger
directory under our contracts
directory. Then, within /contracts/logger
, let’s create a file called discord.ts
.
Here we’ll declare our namespace to TypeScript.
declare module '@ioc:Logger/Discord' { import Logger from 'providers/LogProvider/discord' const DiscordLogger: Logger export default DiscordLogger }
Copied!
- contracts
- logger
- discord.ts
Within our declaration, we’re importing the Logger
type from providers/LogProvider/discord
. Then, we’re stating we’ll have a default export property called DiscordLogger
that’s the type of our Logger
.
Let’s Use It!
The last thing to do is to put it to use! So, let’s head into our App/Controllers/Http/PostsController.ts
file, import it, and use it.
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' import DateService from 'App/Services/DateService' import { inject } from '@adonisjs/fold' import DiscordLogger from '@ioc:Logger/Discord' // 👈 import it using IoC Container @inject() export default class PostsController { public dateService = DateService public async store ({}: HttpContextContract) { const dateTime = DateService.toDateTime() const formattedDate = this.dateService.toDate(dateTime) // 👇 put it to use await DiscordLogger.info('New Post Created', { dateTime }) return `creating a post ${formattedDate}` } // ... }
Copied!
- app
- Controllers
- Http
- PostsController.ts
So, here we’re importing it using the IoC Container from @ioc:Logger/Discord
. Then, we’re using it by calling our info
method.
Boot up your server, and hit your [PostsController.store](<http://PostsController.store>)
route and you should get a fresh Discord message stating a new post was created!
Join The Discussion! (12 Comments)
Please sign in or sign up for free to join in on the dicussion.
Marius
Hello
Thanks for this tutorial
Do you have any advice for using several nested injections ?
Please sign in or sign up for free to reply
tomgobich
Hi Marius!
Thank you for watching/reading!
The only advice I have is to be careful. You don’t want to end up creating a circular dependency. Instead of nesting injections, you can alternatively refactor so your classes extend instead of inject. However, that’s a project design decision. :)
Please sign in or sign up for free to reply
sidneisimmon
Very good! Your tutorial was very helpful. The Adonis documentation still lacks good examples. Success!
#brazilian#dev
Please sign in or sign up for free to reply
tomgobich
Thank you sidneisimmon! Happy to hear it was helpful for you! :)
Please sign in or sign up for free to reply
Anonymous (RhinocerosKrissie443)
This is what I’ve been looking for…
Thank you so much for great tutorial
Please sign in or sign up for free to reply
Anonymous (PigPrissie841)
cool
Please sign in or sign up for free to reply
Anonymous (OcelotLorita168)
good job
Please sign in or sign up for free to reply
tomgobich
Thank you!! :)
Please sign in or sign up for free to reply
Anonymous (PorcupineRenie420)
If I want to utilize the files from `App/Http/Controllers` on a provider file how will I do that?
Please sign in or sign up for free to reply
tomgobich
The convention isn't to use controllers for that. Instead, move the code you want to use out of the controller and into a service file. Then, you can import and use that code inside both your controller, and you can import the service into your provider and use the code there.
Please sign in or sign up for free to reply
Anonymous (SwordtailNadiya472)
This is a great read. Could this technique be used to create a graphQL wrapper?
Please sign in or sign up for free to reply
tomgobich
Thank you! I'm not overly familiar with how GraphQL gets set up, but yeah you'd likely need to use at least a portion of the above technique to get it added to your Adonis project.
Please sign in or sign up for free to reply