Skip to main content
Version: 7.0

Using MikroORM with AdonisJS

This guide covers integrating MikroORM with AdonisJS, replacing the default Lucid ORM with MikroORM's Unit of Work and Identity Map patterns. For a complete working example, see the adonis-example-app repository.

The example app is based on the AdonisJS slim starter kit and uses the same blog domain as the MikroORM Getting Started Guide.

Installation​

Start with an AdonisJS project (e.g. the slim starter kit), then install MikroORM:

npm install @mikro-orm/core @mikro-orm/sqlite # or any other driver
npm install -D @mikro-orm/cli @mikro-orm/migrations @mikro-orm/seeder

MikroORM Configuration​

Create a MikroORM config file. This is used both by the application and the CLI (for migrations):

config/mikro-orm.config.ts
import { defineConfig } from '@mikro-orm/sqlite'
import { Migrator } from '@mikro-orm/migrations'
import { SeedManager } from '@mikro-orm/seeder'
import { User } from '#entities/user'
import { ArticleSchema } from '#entities/article'

export default defineConfig({
dbName: 'sqlite.db',
entities: [User, ArticleSchema],
extensions: [Migrator, SeedManager],
debug: true,
})

Add the CLI configuration to your package.json:

{
"mikro-orm": {
"useTsNode": true,
"configPaths": [
"./config/mikro-orm.config.ts",
"./build/config/mikro-orm.config.js"
]
}
}

Service Provider​

The service provider registers MikroORM, EntityManager, and repositories in the AdonisJS IoC container. Note the imports from the driver package (@mikro-orm/sqlite) so the EntityManager type includes query builder support:

providers/mikro_orm_provider.ts
import { MikroORM, EntityManager } from '@mikro-orm/sqlite'
import type { ApplicationService } from '@adonisjs/core/types'
import config from '#config/mikro-orm.config'
import { UserRepository } from '#repositories/user_repository'
import { ArticleRepository } from '#repositories/article_repository'
import { User } from '#entities/user'
import { ArticleSchema } from '#entities/article'

export default class MikroOrmProvider {
constructor(protected app: ApplicationService) {}

register() {
// MikroORM v7 supports synchronous initialization
this.app.container.singleton(MikroORM, () => {
return new MikroORM(config)
})

// import EntityManager from the driver package for query builder support
this.app.container.singleton(EntityManager, async (resolver) => {
const orm = await resolver.make(MikroORM)
return orm.em
})

// register custom repositories
this.app.container.singleton(UserRepository, async (resolver) => {
const orm = await resolver.make(MikroORM)
return orm.em.getRepository(User)
})

this.app.container.singleton(ArticleRepository, async (resolver) => {
const orm = await resolver.make(MikroORM)
return orm.em.getRepository(ArticleSchema)
})
}

async boot() {
const orm = await this.app.container.make(MikroORM)
// for simplicity, we use `schema.update()` to auto-create/update tables.
// in production, use `orm.migrator.up()` with proper migrations instead
await orm.schema.update()
}

async shutdown() {
const orm = await this.app.container.make(MikroORM)
await orm.close()
}
}

Register the provider in adonisrc.ts:

adonisrc.ts
import { defineConfig } from '@adonisjs/core/app'

export default defineConfig({
providers: [
() => import('@adonisjs/core/providers/app_provider'),
() => import('@adonisjs/core/providers/hash_provider'),
() => import('#providers/mikro_orm_provider'),
],
preloads: [() => import('#start/routes'), () => import('#start/kernel')],
// ...
})

RequestContext Middleware​

The most critical part of the integration is the RequestContext middleware. It ensures each HTTP request gets its own identity map, preventing entity state from leaking between requests:

app/middleware/mikro_orm_middleware.ts
import { RequestContext } from '@mikro-orm/core'
import { MikroORM } from '@mikro-orm/sqlite'
import { inject } from '@adonisjs/core'
import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'

@inject()
export default class MikroOrmMiddleware {
constructor(protected orm: MikroORM) {}

handle(_ctx: HttpContext, next: NextFn) {
return RequestContext.create(this.orm.em, () => next())
}
}

Register it as server-level middleware in start/kernel.ts so it runs on every request:

start/kernel.ts
import router from '@adonisjs/core/services/router'
import server from '@adonisjs/core/services/server'

server.errorHandler(() => import('#exceptions/handler'))

server.use([
() => import('#middleware/container_bindings_middleware'),
() => import('#middleware/mikro_orm_middleware'),
])

router.use([() => import('@adonisjs/core/bodyparser_middleware')])

RequestContext.create() wraps the next() call, so all downstream middleware and the controller handler run within the request context's async local storage scope.

Error Handling​

Handle MikroORM's NotFoundError (thrown by findOneOrFail) in the AdonisJS exception handler:

app/exceptions/handler.ts
import app from '@adonisjs/core/services/app'
import { HttpContext, ExceptionHandler } from '@adonisjs/core/http'
import { NotFoundError } from '@mikro-orm/core'

export default class HttpExceptionHandler extends ExceptionHandler {
protected debug = !app.inProduction

async handle(error: unknown, ctx: HttpContext) {
if (error instanceof NotFoundError) {
ctx.response.status(404).send({ error: error.message })
return
}

return super.handle(error, ctx)
}
}

Entity Definitions​

Define entities using defineEntity for full type inference:

app/entities/base.ts
import { defineEntity, p } from '@mikro-orm/core'

export const BaseSchema = defineEntity({
name: 'BaseEntity',
abstract: true,
properties: {
id: p.integer().primary(),
createdAt: p.datetime().onCreate(() => new Date()),
updatedAt: p.datetime().onCreate(() => new Date()).onUpdate(() => new Date()),
},
})
app/entities/article.ts
import { defineEntity, type InferEntity, p } from '@mikro-orm/core'
import { BaseSchema } from '#entities/base'
import { User } from '#entities/user'
import { ArticleRepository } from '#repositories/article_repository'

export const ArticleSchema = defineEntity({
name: 'Article',
extends: BaseSchema,
repository: () => ArticleRepository,
properties: {
slug: p.string().unique(),
title: p.string().index(),
text: p.text().lazy(),
author: () => p.manyToOne(User).ref(),
},
})

export type IArticle = InferEntity<typeof ArticleSchema>

Adding Methods and Hooks​

To add custom methods to an entity, extend the generated class via setClass. Lifecycle hooks are added via addHook:

app/entities/user.ts
import { defineEntity, type EventArgs, p } from '@mikro-orm/core'
import { hash, verify } from 'argon2'
import { BaseSchema } from '#entities/base'

export const UserSchema = defineEntity({
name: 'User',
extends: BaseSchema,
properties: {
fullName: p.string(),
email: p.string(),
password: p.string().hidden().lazy(),
},
})

export class User extends UserSchema.class {
async verifyPassword(password: string) {
return verify(this.password, password)
}
}

UserSchema.setClass(User)

async function hashPassword(args: EventArgs<User>) {
const password = args.changeSet?.payload.password

if (typeof password === 'string') {
args.entity.password = await hash(password)
}
}

UserSchema.addHook('beforeCreate', hashPassword)
UserSchema.addHook('beforeUpdate', hashPassword)

Using in Controllers​

Controllers inject EntityManager and repositories directly via @inject(). The RequestContext middleware ensures the injected EntityManager resolves to the correct request-scoped fork automatically:

app/controllers/articles_controller.ts
import { inject } from '@adonisjs/core'
import type { HttpContext } from '@adonisjs/core/http'
import { EntityManager } from '@mikro-orm/sqlite'
import { ArticleRepository } from '#repositories/article_repository'

@inject()
export default class ArticlesController {
constructor(
protected em: EntityManager,
protected articleRepo: ArticleRepository,
) {}

async index({ request }: HttpContext) {
const limit = request.input('limit')
const offset = request.input('offset')
const { items, total } = await this.articleRepo.listArticles({ limit, offset })
return { items, total }
}

async show({ params }: HttpContext) {
return this.articleRepo.findOneOrFail({ slug: params.slug }, {
populate: ['author', 'text'],
})
}

async store({ auth, request }: HttpContext) {
const author = auth.getUserOrFail()
const { title, text } = request.body()
const article = this.articleRepo.create({ title, text, author })
await this.em.flush()
return article
}
}

Routes​

Register routes in start/routes.ts using lazy controller imports. Protected routes use the auth middleware:

start/routes.ts
import router from '@adonisjs/core/services/router'
import { middleware } from '#start/kernel'

const ArticlesController = () => import('#controllers/articles_controller')

// public routes
router.get('/article', [ArticlesController, 'index'])
router.get('/article/:slug', [ArticlesController, 'show'])

// protected routes
router.group(() => {
router.post('/article', [ArticlesController, 'store'])
router.patch('/article/:id', [ArticlesController, 'update'])
router.delete('/article/:id', [ArticlesController, 'destroy'])
}).use(middleware.auth())

Authentication​

Instead of implementing custom JWT authentication, you can use AdonisJS's built-in @adonisjs/auth package with a custom user provider backed by MikroORM. This gives you session-based auth with full framework integration.

Setup​

Install the auth and session packages:

npm install @adonisjs/auth @adonisjs/session

Register the providers in adonisrc.ts:

adonisrc.ts
export default defineConfig({
providers: [
// ...
() => import('@adonisjs/session/session_provider'),
() => import('@adonisjs/auth/auth_provider'),
() => import('#providers/mikro_orm_provider'),
],
})

User Provider​

Implement SessionUserProviderContract to bridge MikroORM with the AdonisJS session guard. The findById method uses the EntityManager, which resolves to the request-scoped fork via RequestContext:

app/auth/mikro_orm_user_provider.ts
import { symbols } from '@adonisjs/auth'
import type { SessionGuardUser, SessionUserProviderContract } from '@adonisjs/auth/types/session'
import type { EntityManager } from '@mikro-orm/sqlite'
import { UserSchema, User } from '#entities/user'

export class MikroOrmUserProvider implements SessionUserProviderContract<User> {
declare [symbols.PROVIDER_REAL_USER]: User

constructor(private em: EntityManager) {}

async createUserForGuard(user: User): Promise<SessionGuardUser<User>> {
return {
getId() {
return user.id
},
getOriginal() {
return user
},
}
}

async findById(identifier: number): Promise<SessionGuardUser<User> | null> {
const user = await this.em.findOne(UserSchema, identifier)

if (!user) {
return null
}

return this.createUserForGuard(user as User)
}
}

Auth Configuration​

Configure the session guard with the custom MikroORM provider using configProvider.create for lazy resolution:

config/auth.ts
import { defineConfig } from '@adonisjs/auth'
import { sessionGuard } from '@adonisjs/auth/session'
import { configProvider } from '@adonisjs/core'
import { EntityManager } from '@mikro-orm/sqlite'
import type { InferAuthenticators } from '@adonisjs/auth/types'

const authConfig = defineConfig({
default: 'web',
guards: {
web: sessionGuard({
useRememberMeTokens: false,
provider: configProvider.create(async (app) => {
const { MikroOrmUserProvider } = await import('#auth/mikro_orm_user_provider')
const em = await app.container.make(EntityManager)
return new MikroOrmUserProvider(em)
}),
}),
},
})

export default authConfig

declare module '@adonisjs/auth/types' {
interface Authenticators extends InferAuthenticators<typeof authConfig> {}
}

Middleware Stack​

Register session and auth middleware in start/kernel.ts. The InitializeAuthMiddleware adds ctx.auth to every request. The named auth middleware protects specific routes:

start/kernel.ts
import router from '@adonisjs/core/services/router'
import server from '@adonisjs/core/services/server'

server.errorHandler(() => import('#exceptions/handler'))

server.use([
() => import('#middleware/container_bindings_middleware'),
() => import('#middleware/mikro_orm_middleware'),
() => import('@adonisjs/session/session_middleware'),
() => import('@adonisjs/auth/initialize_auth_middleware'),
])

router.use([() => import('@adonisjs/core/bodyparser_middleware')])

export const middleware = router.named({
auth: () => import('#middleware/auth_middleware'),
})

The auth middleware uses the framework's authenticateUsing method:

app/middleware/auth_middleware.ts
import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'
import type { Authenticators } from '@adonisjs/auth/types'

export default class AuthMiddleware {
async handle(ctx: HttpContext, next: NextFn, options: { guards?: (keyof Authenticators)[] } = {}) {
await ctx.auth.authenticateUsing(options.guards || [ctx.auth.defaultGuard])
return next()
}
}

Using Auth in Controllers​

Use ctx.auth to log users in and access the authenticated user:

app/controllers/users_controller.ts
@inject()
export default class UsersController {
constructor(protected em: EntityManager, protected userRepo: UserRepository) {}

async signIn({ request, response, auth }: HttpContext) {
const { email, password } = request.body()
const user = await this.userRepo.login(email, password)
await auth.use('web').login(user)
return user
}

async profile({ auth }: HttpContext) {
return auth.getUserOrFail()
}
}

Testing​

Tests use Japa (AdonisJS's test runner). Register the sessionApiClient and authApiClient plugins for session-based auth testing:

tests/bootstrap.ts
import { assert } from '@japa/assert'
import { apiClient } from '@japa/api-client'
import app from '@adonisjs/core/services/app'
import type { Config } from '@japa/runner/types'
import { pluginAdonisJS } from '@japa/plugin-adonisjs'
import { authApiClient } from '@adonisjs/auth/plugins/api_client'
import { sessionApiClient } from '@adonisjs/session/plugins/api_client'
import testUtils from '@adonisjs/core/services/test_utils'

export const plugins: Config['plugins'] = [
assert(),
apiClient(),
pluginAdonisJS(app),
sessionApiClient(app),
authApiClient(app),
]

The authApiClient plugin adds a loginAs helper that authenticates requests via sessions — no manual token handling needed:

tests/functional/user.spec.ts
import { test } from '@japa/runner'
import app from '@adonisjs/core/services/app'
import { MikroORM } from '@mikro-orm/sqlite'
import { UserSchema, User } from '#entities/user'

test.group('User', () => {
test('sign in with valid credentials', async ({ client }) => {
const response = await client.post('/user/sign-in').json({
email: 'foo@bar.com',
password: 'password123',
})

response.assertStatus(200)
response.assertBodyContains({ fullName: 'Foo Bar' })
})

test('update profile', async ({ client, assert }) => {
const orm = await app.container.make(MikroORM)
const user = await orm.em.fork().findOneOrFail(UserSchema, { email: 'foo@bar.com' })

const response = await client.patch('/user/profile').loginAs(user as User).json({
bio: 'Updated bio text',
})

response.assertStatus(200)
assert.equal(response.body().bio, 'Updated bio text')
})
})

Example​

A full working example with users, articles, comments, tags, soft delete, virtual entities, and tests can be found in the adonis-example-app repository.