8 minutes
Adonis V5 (MongoDB) provider
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!
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! 😅
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!
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 Mongoose, { Schema } from '@ioc:Mongoose'
export default Mongoose.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!
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
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
1623 Words
2021-02-18 11:41 +0000