Static, Non-Static, & Singleton Services

We'll learn what services are. We'll then discuss three different ways we can use them within our AdonisJS application, including static, non-static, and singleton services.

Published
Jan 24, 22
Duration
15m 14s

Developer, dog lover, and burrito eater. Currently teaching AdonisJS, a fully featured NodeJS framework, and running Adocasts where I post new lessons weekly. Professionally, I work with JavaScript, .Net C#, and SQL Server.

Adocasts

Burlington, KY

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.

  1. Using static methods

  2. Using non-static methods

  3. 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'
  }

}
  1. First, weโ€™re importing our DateService from App/Services/DateService.

  2. 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'
  }

}
  1. First, weโ€™re defining a property called dateService of type DateService on our PostsController.

  2. Then, within the constructor, weโ€™re creating a new instance of our DateService and store that instance on our controllerโ€™s instance.

  3. 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'
  }

}
  1. First, weโ€™re importing the inject decorator from @adonisjs/fold.

  2. Then, weโ€™re defining that our service should be publicly accessibleCalled dateServiceBe instantiated with and of type DateService

  3. Publicly accessible

  4. Called dateService

  5. 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.

  1. Create a new instance of our PostsController

  2. 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:

  1. Contains a non-static count property thatโ€™s instantiated to zero when our service instance is created.

  2. 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.

  3. 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.

  4. 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'
  }
}
  1. First, weโ€™ll import it from App/Services/CounterService.

  2. 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.

  1. Anonymous (MiteCati572)
    Commented 2 years ago

    Nice jobe. Appreciate what you doing for the community..

    0

    Please sign in or sign up for free to reply

    1. Commented 2 years ago

      Thank you!! :)

      0

      Please sign in or sign up for free to reply

  2. Commented 2 years ago

    Very good! Your tutorial was very helpful. The Adonis documentation still lacks good examples. Success!

    0

    Please sign in or sign up for free to reply

    1. Commented 2 years ago

      Thank you, happy to be able to help!! :)

      0

      Please sign in or sign up for free to reply

  3. Commented 1 year ago

    ๐Ÿ‘๐Ÿ‘

    1

    Please sign in or sign up for free to reply

  4. matias
    Commented 1 year ago

    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?

    0

    Please sign in or sign up for free to reply

  5. Commented 11 months ago

    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?

    1

    Please sign in or sign up for free to reply

    1. Commented 11 months ago

      Thank you, Raymond! Yes, those code blocks should've been using the dateTime result from the line above. Here's the corrected usage:

      const dateTime = DateService.toDateTime()
      const date = this.dateService.toDate(dateTime) // ๐Ÿ‘ˆ use it 
      0

      Please sign in or sign up for free to reply

  6. Commented 11 months ago

    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.

    1

    Please sign in or sign up for free to reply

    1. Commented 11 months ago

      Thank you very much, Zubs! That means a lot!!

      0

      Please sign in or sign up for free to reply