When working with controllers there will come a time when you need to reuse the same line or block of code multiple times. This would be a great use case for extracting that portion of code we want to reuse out of our controller(s) and into a service. Services are classes that contain reusable methods, which we can import into our controllers or throughout our application and use as many times as needed.
Another use case for services comes out of code structure preference. Some people prefer to have their controllers only accept, validate, call services, and return back a response. This keeps controllers slim and keeps your code easily reusable since all the business logic is extracted into services.
Multiple Types of Services
There are multiple different ways we can utilize services with AdonisJS. For the purposes of this lesson, we’ll be focusing on the following three.
Using static methods
Using non-static methods
Using singletons (the same service instance for each request)
1. Static Method Services
Using services with static methods is the easiest and most straightforward approach. For this, we’ll define a class and within this class, we’ll define our methods as static methods. Static methods don’t require creating an instance of the class in order to use them. So, they can be used as though they were some method on an object.
Defining the Service Method
For example, let’s create a folder called Services
within our app
directory. Then, let’s create a file within Services
called DateService.ts
.
Then, let’s create a class called DateService
that is the default export for the file.
// app/Services/DateService.ts
export default class DateService {
}
Next, let’s define a static method called toDateTime
. It’ll take in a date and a time, both of which default to null if not provided. Then, if a date or time is provided it’ll build a Luxon DateTime using those values.
import { DateTime } from 'luxon'
export default class DateService {
public static toDateTime(date: DateTime | undefined | null = null, time: DateTime | undefined | null = null) {
let dateTime = DateTime.now()
if (date) {
dateTime = dateTime.set({ year: date.year, month: date.month, day: date.day })
}
if (time) {
dateTime = dateTime.set({ hour: time.hour, minute: time.minute, second: time.second })
}
return dateTime
}
}
Since toDateTime
is a static method, we can simply access it by importing our DateService
anywhere in our AdonisJS application and calling the method.
Using the Service Method
Let’s say we wanted to use this within our PostsController.store
method. Maybe we have a publishAtDate
and publishAtTime
field on the front end using the native date and time inputs, so these two values come up separately.
We can import our DateService
at the top of our PostsController
and directly call our toDateTime
method within our PostsController.store
method.
// app/Controllers/Http/PostsController.ts
import DateService from 'App/Services/DateService' // 👈 import
export default class PostsController {
public async store ({}: HttpContextContract) {
const dateTime = DateService.toDateTime() // 👈 use
return 'creating a post'
}
}
First, we’re importing our DateService from App/Services/DateService.
Then, we’re directly calling toDateTime without instantiating an instance of the DateService class, since it’s a static method.
You can place however many static methods you’d like on your services and use this same approach for any service class using static methods.
2. Non-Static Method Services
Using services with non-static methods is similar to the static method approach, the main difference is we won’t use the static
keyword when defining our methods. We’ll also need to create a new instance of our service class in order to actually use our non-static methods.
Additionally, you can mix and match static and non-static methods on a single service class if you wish. Static methods are great when you can easily provide all information the method needs via arguments. Non-static methods come in handy if you need to store data on the class itself to share information across methods or from the constructor.
Defining The Service Method
So, for this example, let’s add a non-static method to our DateService
that takes a DateTime and returns back just the date.
import { DateTime } from 'luxon'
export default class DateService {
// our static service method
public static toDateTime(date: DateTime | undefined | null = null, time: DateTime | undefined | null = null) {
// ...
}
// 👇 our new non-static service method
public toDate(dateTime: DateTime) {
return dateTime.toFormat('MM dd yyyy')
}
}
Now, the toDate
method we’ve added above is actually a great candidate for a static method because it doesn’t rely on anything outside the method. However, for demonstration purposes, we’ll be defining it as a non-static method here.
Using the Service Method
Next, let’s use it within our PostsController.store
method. Since it’s a non-static method, however, we’re going to need an instance of our DateService
in order to use our toDate
method.
// app/Controllers/Http/PostsController.ts
import DateService from 'App/Services/DateService' // 👈 import the service
export default class PostsController {
public dateService: DateService // 👈 define it as a property on the class
constructor() {
this.dateService = new DateService() // 👈 create & store a new instance
}
public async store ({}: HttpContextContract) {
const dateTime = DateService.toDateTime()
const date = this.dateService.toDate(dateTime) // 👈 use it
return 'creating a post'
}
}
First, we’re defining a property called dateService of type DateService on our PostsController.
Then, within the constructor, we’re creating a new instance of our DateService and store that instance on our controller’s instance.
Lastly, since we stored the DateService instance on our controller’s instance we can access it via this to call our service method at this.dateService.toDate(), which we’re providing a Luxon DateTime set to now.
Since our DateService
is simple and doesn't need any information passed into the constructor you could directly instantiate an instance of the DateService
when declaring the class property, like the one below.
export default class PostsController {
public dateService = new DateService()
}
Using AdonisJS Fold
Above we manually instantiated an instance of our DateService
and set it onto our controller’s instance via the constructor. However, we can alternatively use AdonisJS Fold to inject an instantiated instance for us.
AdonisJS Fold has a decorator method called inject
, that we can use to decorate our controller. Then, we just need to define the visibility, property name, and type within the constructor’s arguments. AdonisJS Fold will take care of the rest for us.
// app/Controllers/Http/PostsController.ts
import DateService from 'App/Services/DateService'
import { inject } from '@adonisjs/fold' 👈 import the inject decorator
@inject() // 👈 decorate your controller with it
export default class PostsController {
// 👇 set the visibility, name, and type to be injected
constructor(public dateService: DateService) {}
public async store ({}: HttpContextContract) {
const dateTime = DateService.toDateTime()
const date = this.dateService.toDate(dateTime)
return 'creating a post'
}
}
First, we’re importing the inject decorator from @adonisjs/fold.
Then, we’re defining that our service should be publicly accessibleCalled dateServiceBe instantiated with and of type DateService
Publicly accessible
Called dateService
Be instantiated with and of type DateService
You can think of the above as shorthand for our previous example because with both we’ll still have a new instance of our DateService
placed on our PostsController
. Below is a side-by-side of the two approaches.
// manually instantiating
export default class PostsController {
public dateService: DateService
constructor() {
this.dateService = new DateService()
}
}
// injecting with AdonisJS Fold
@inject()
export default class PostsController {
constructor(public dateService: DateService) {}
}
The Flow of a Request
When our AdonisJS server receives a request for our PostsController.store
method. AdonisJS will do the following.
Create a new instance of our PostsController
This will then, create a new instance of our DateService
So, a new instance of our service will be created for each request. That’s perfectly fine for most cases, however, in some cases, you’ll need your service to maintain its instance. It may be connecting to an external service or something of the sort.
For that, we’ll want to create our service as a singleton.
3. Using Singletons
If we need the same instance of our service to be used for each and every request our server receives, we’ll want to create our services as singletons. To do this, all we need to do is export a new instance of our service from the service file, like below.
// app/Services/CounterService.ts
class CounterService {
public count = 0 // 👈 1. maintain a count
constructor() {
console.log('CounterService > instantiated') // 👈 2. log when an instance is created
}
public increment() { // 👈 3. increment the count
this.count += 1
return this.count
}
}
export default new CounterService() // 👈 4. export as a singleton
Above we’ve created a new service, called CounterService
, that:
Contains a non-static count property that’s instantiated to zero when our service instance is created.
Console logs a message every time the service is instantiated. This is so we can verify in the console that our service only has one instance instantiated.
Contains a non-static increment method that increments the service’s count by one and returns the new count value. We’ll use this to demonstrate our instance is maintained for each request.
We export a new instance of our service directly in the file, this is what makes this service a singleton and ensures only one instance will be created and used throughout our server’s life.
Using the Singleton Service
Next, let’s put our singleton service to use!
// app/Controllers/Http/PostsController.ts
import DateService from 'App/Services/DateService'
import CounterService from 'App/Services/CounterService' // 👈 import it
import { inject } from '@adonisjs/fold'
@inject()
export default class PostsController {
constructor(public dateService: DateService) {}
public async store ({}: HttpContextContract) {
const dateTime = DateService.toDateTime()
const date = this.dateService.toDate(dateTime)
const count = CounterService.increment() // 👈 use it!
console.log({ count })
return 'creating a post'
}
}
First, we’ll import it from App/Services/CounterService.
Then, we’ll use it by calling our increment. Additionally, let’s also console log the returned incremented count. We’ll use this in a second.
Alternatively, if you prefer for the service to be on your class instance, you can set it as a property directly on the class.
// app/Controllers/Http/PostsController.ts
import DateService from 'App/Services/DateService'
import CounterService from 'App/Services/CounterService'
import { inject } from '@adonisjs/fold'
@inject()
export default class PostsController {
public counterService = CounterService // 👈 set on class
constructor(public dateService: DateService) {}
public async store ({}: HttpContextContract) {
const dateTime = DateService.toDateTime()
const date = this.dateService.toDate(dateTime)
const count = this.counterService.increment() // 👈 use it!
console.log({ count })
return 'creating a post'
}
}
Note that we’re not creating a new instance of our CounterService
, but rather just setting it like any other variable value, since we’ve already created the instance within the service file.
Testing The Singleton
Lastly, let’s test and see the difference between our DateService
and singleton CounterService
. In order to best drive this point home, let’s add a console log when a DateService
instance is created as well.
import { DateTime } from 'luxon'
export default class DateService {
constructor() {
console.log('DateService > instantiated') // 👈
}
public static toDateTime(date: DateTime | undefined | null = null, time: DateTime | undefined | null = null) {
// ...
}
public toDate(dateTime: DateTime) {
// ...
}
}
Next, start up your server with npm run dev
, make a request to your PostsController.store
route and you should see something like the following in your terminal.
CounterService > instantiated
[1642944693179] INFO (lets-learn-adonis-5/12102 on toms-mbp.lan): started server on 0.0.0.0:3333
╭────────────────────────────────────────────────────────╮
│ │
│ Server address: <http://127.0.0.1:3333> │
│ Watching filesystem for changes: YES │
│ Encore server address: <http://localhost:8080> │
│ │
╰────────────────────────────────────────────────────────╯
DateService > instantiated
1
Notice our CounterService
is instantiated while our server is booting. This is because the instance is created when AdonisJS is resolving imports. When our service is imported, that’s when our CounterService
is instantiated. Whereas, our DateService
is only instantiated when a request comes through and for each request.
If we make two more requests to our PostsController.store
route, we should see that our DateService
is instantiated two times and our CounterService
is not. Additionally, we’ll see our count increment for each request, meaning the same instance is used for each request.
CounterService > instantiated
[1642944693179] INFO (lets-learn-adonis-5/12102 on toms-mbp.lan): started server on 0.0.0.0:3333
╭────────────────────────────────────────────────────────╮
│ │
│ Server address: <http://127.0.0.1:3333> │
│ Watching filesystem for changes: YES │
│ Encore server address: <http://localhost:8080> │
│ │
╰────────────────────────────────────────────────────────╯
DateService > instantiated
1
DateService > instantiated
2
DateService > instantiated
3
Pretty cool, huh?!
Join The Discussion! (10 Comments)
Please sign in or sign up for free to join in on the dicussion.
Anonymous (MiteCati572)
Nice jobe. Appreciate what you doing for the community..
Please sign in or sign up for free to reply
tomgobich
Thank you!! :)
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!
Please sign in or sign up for free to reply
tomgobich
Thank you, happy to be able to help!! :)
Please sign in or sign up for free to reply
luiza-marlene
👏👏
Please sign in or sign up for free to reply
matias
How would you test/mock these services? Let's say you have a controller calling one of the methods of the singleton, and that method is doing a console.log or some side effect. How would you go about not calling the side effect?
Please sign in or sign up for free to reply
raymondcamden
While following along, in "Using the Service Method", is this line incorrect:
const date = this.dateService.toDate(DateTime.now())
Since DateTime hasn't been defined in PostsController.ts?
Please sign in or sign up for free to reply
tomgobich
Thank you, Raymond! Yes, those code blocks should've been using the
dateTime
result from the line above. Here's the corrected usage:Please sign in or sign up for free to reply
Zubs
This is a very great tutorial video. I've always read about singletons and have always tried to understand them, but this explanation is the best I've ever seen. Thank you very much.
Please sign in or sign up for free to reply
tomgobich
Thank you very much, Zubs! That means a lot!!
Please sign in or sign up for free to reply