Story time!

You want to use MongoDB with AdonisJS, but no idea how to do it?

U’ve come exactly to the right place then!

WARNING! A lot of code ahead!

Not so hard

Overview

  • Create Adonis project
  • Use MongoDB with Mongoose like you’d use any other Node package
  • Turn it into provider
  • Not going to publish it on NPM (I don’t want the maintenance burden, but feel free to do it)

Create Adonis project

npm init adonis-ts-app mongoose-project

Oof-oof! Already so much files and code appeared! 😅

Many hard

If you get any issues then refer to documentation or Github discussions

Let’s start using Mongoose!

First of all, you have to install Mongoose and start MongoDB instance

Inside the project folder npm i mongoose

Now we actually need a MongoDB to connect to. Since I already have Docker installed I’m just going to spin up MongoDB in container

docker run --network host mongo

Using the network host for several reasons, which is worth another blog post… If you don’t want to use host networking then you can expose port directly too with -p 27017:27017

Alright, all preparations done. Let’s dig into the code now!

Ready

Using Mongoose directly like any other package

Let’s create some super simple routes and some controller

// start/routes.ts
Route.get('/cats', 'CatsController.index')
Route.get('/dogs', 'DogsController.index')
// app/Controllers/Http/CatsController
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'

export default class CatsController {
  public async index ({ }: HttpContextContract) {
    return 'I am cat'
  }
}

// app/Controllers/Http/DogsController
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'

export default class DogsController {
  public async index ({ }: HttpContextContract) {
    return 'I am dog'
  }
}

Now, let’s add Mongoose to the mix finally!

For that we need to import Mongoose, initialize connection and set our first models

Starting with CatsController

// app/Controllers/Http/CatsController
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
// Import Mongoose
import mongoose, { Schema } from 'mongoose'

// Create cat model
const Cat = mongoose.model('Cat', new Schema({
  name: String,
}))

export default class CatsController {
  public async index ({ }: HttpContextContract) {
    // Initialize connection to database
    await mongoose.connect('mongodb://localhost/my_database', {
      useNewUrlParser: true,
      useUnifiedTopology: true,
      useFindAndModify: false,
      useCreateIndex: true,
    })

    // Create a cat with random name
    const cat = new Cat({
      name: Math.random().toString(36).substring(7),
    })
    // Save cat to DB
    await cat.save()

    // Return list of all saved cats
    const cats = await Cat.find()

    // Close the connection
    await mongoose.connection.close()

    // Return all the cats (including the new one)
    return cats
  }
}

Soo.. It seems to be working all fine.. But can you find 10 things that’s wrong with it?

Oke, actually lil bit less than 10 things. But main problem is database connection

In the example above with every incoming request we initialize new database connection, do some stuff and then close the connection

Let’s upgrade this lil bit, so connection is shared across requests

// app/Controllers/Http/CatsController
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
// Import Mongoose
import mongoose, { Schema } from 'mongoose'

// Initialize connection to database
// Notice how it doesn't have 'await'
// We won't need it, coz Mongoose "queues" any database queries
// until connection is made. It's nice to have, but might with 
// our current implementation it might start to cause some troubles in prod,
// especially when DB is on other server
mongoose.connect('mongodb://localhost/my_database', {
  useNewUrlParser: true,
  useUnifiedTopology: true,
  useFindAndModify: false,
  useCreateIndex: true,
})

// Create cat model
const Cat = mongoose.model('Cat', new Schema({
  name: String,
}))

export default class CatsController {
  public async index ({ }: HttpContextContract) {
    // Create a cat with random name
    const cat = new Cat({
      name: Math.random().toString(36).substring(7),
    })
    // Save cat to DB
    await cat.save()

    // Return list of all saved cats
    const cats = await Cat.find()

    // No need to close to connection, we'll keep it alive forever
    // await mongoose.connection.close()

    // Return all the cats (including the new one)
    return cats
  }
}

Cool. A lot better. Now connection to DB is initialized when first request comes in and is kept open forever (or until process dies)

Let’s copy-paste the same thing to DogsController and rename Cat to Dog

Hitting both endpoints seems to work fine too. But when looking at MongoDB connections, then every controller creates their own connection to DB. Which isn’t too nice, would be cool to re-use the same connection

Also when server is shutting down there’s no way to graceful shutdown. That’s another thing we need to handle in provider

Enter the Provider!

Let’s create us a provider in project

node ace make:provider MongoProvider

We are going to use provider register and shutdown methods

Register for initializing connection and shutdown for clean disconnection from server

// providers/MongoProvider.ts
public register () {
  // Create new Mongoose instance
  const mongoose = new Mongoose()

  // Connect the instance to DB
  mongoose.connect('mongodb://localhost/my_database', {
    useNewUrlParser: true,
    useUnifiedTopology: true,
    useFindAndModify: false,
    useCreateIndex: true,
  })

  // Attach it to IOC container as singleton
  this.application.container.singleton('Mongoose', () => mongoose)
}

public async shutdown () {
  // Cleanup, since app is going down
  // Going to take the Mongoose singleton from container
  // and call disconnect() on it
  // which tells Mongoose to gracefully disconnect from MongoBD server
  await this.application.container.use('Mongoose').disconnect()
}

We also need to set type definitions for it. Otherwise TS will have no idea what’s going on

For that need to create new file under contracts and declare module in there

// contracts/Mongoose.ts
// Declare @ioc:Mongoose module
declare module '@ioc:Mongoose' {
  // Export everything from Mongoose
  // Since that's what our provider is doing
  export * from 'mongoose'
}

With everything setup we can now refactor our controllers to use our new Mongoose provider

// app/Controllers/Http/CatsController
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
// Import our own cool Mongoose module
import Mongoose, { Schema } from '@ioc:Mongoose'

// Create cat model
const Cat = Mongoose.model('Cat', new Schema({
  name: String,
}))


export default class CatsController {
  public async index ({ }: HttpContextContract) {
    // Create a cat with random name
    const cat = new Cat({
      name: Math.random().toString(36).substring(7),
    })

    // Save cat to DB
    await cat.save()

    // Return list of all saved cats
    const cats = await Cat.find()

    // Return all the cats (including the new one)
    return cats
  }
}

That’s a lot cleaner + we got some performance improvements and are also handling disconnecting from DB server

Let’s also move those models out of here. Doesn’t look good defining models in controller

Just going to make app/Models/Mongoose/Cat.ts and app/Models/Mongoose/Dog.ts and throw them in there

import { Schema, model } from '@ioc:Mongoose'

export default model('Cat',
  new Schema({
    name: String,
  })
)

This allows us to make another wave of controllers refactoring

import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
// Import our cool cat model
import Cat from 'App/Models/Mongoose/Cat'

export default class CatsController {
  public async index ({ }: HttpContextContract) {
    // Create a cat with random name
    const cat = new Cat({
      name: Math.random().toString(36).substring(7),
    })

    // Save cat to DB
    await cat.save()

    // Return list of saved cats
    return Cat.find()
  }
}

That’s a lot cleaner than it used to be at the beginning

How to turn it into external sharable provider

Since developers are lazy we don’t want to recreate the same thing over and over again in different projects

That’s why it’s good to make something widely used like Mongoose provider as separate standalone thing, so we can import it in all the projects!

All modules

It’s actually easier than it sounds

First of all, let’s create new folder and install @adonis/mrm-present to there

npm i -D mrm @adonisjs/mrm-preset

After that add package.json with script like this and run it

{
 "scripts": {
   "mrm": "mrm --preset=@adonisjs/mrm-preset"
 }
}

After or before that you can also do npm init to walk thro another CLI to setup stuff like keywoards, versions etc

Since I don’t need everything I’m just going to run npm run mrm editorconfig eslint package

Install mongoose in there as dependency

Also need to install @adonisjs/core as dev-dependency npm i -D @adonisjs/core@alpha

Now, need to update tsconfig.json to pick up AdonisJS/core types, by adding types inside compilerOptions

{
  "compilerOptions": {
    ... ,
    "types": [
      "@types/node",
      "@adonisjs/core"
    ]
  }
}

Finally we can create our provider file providers/MongooseProvider.ts This file will hold all the content from our previous providers/MongoProvider.ts (safe to copy-paste)

Depending on what you made as your “main” entrypoint when setting up project, might need to change that in package.json too. Main entry point should be "build/providers/MongooseProvider.js"

Almost done. Just add 2 more files for type definitions

// adonis-typings/index.ts
// This we give as single entrypoint to typings and
//  point it to also read them from MongooseProvider
/// <reference path="./MongooseProvider.ts" />

Next we need to create adonis-typings/MongooseProvider.ts which has exactly the same contents from our previous contracts/Mongoose.ts

Now, final thing to do is set the typings path in package.json

"typings": "./build/adonis-typings/index.d.ts",

Beeeeh! Finally! We have it ready!

Run npm run build and it will give you nice build folder

Party time

Now it’s up to you, if you want to publish this on NPM or install it directly from Gitlab / Github. External Mongoose provider is ready and you can use it on all your future AdonisJS projects!

Btw, source code of the provider is available in here: https://gitlab.com/McSneaky/mongoose-provider

Another quality of life thing to do is to add "adonisjs" object to your provider package.json

"adonisjs": {
  "types": "mongoose-provider",
  "providers": [
    "mongoose-provider"
  ]
}

Where types and providers point to your package name. This allows your package consumers to just call node ace invoke mongoose-provider and it will update their tsconfig.json with correct types and add your provider to consumer .adonisrc.json file

Now to use the package just install it npm install mongoose-provider, invoke it node ace invoke mongoose-provider and then where you want to use Mongoose import it import Mongoose from '@ioc:Mongoose'

PS: mongoose-provider is not published to NPM, it’s just an example