├── .github ├── FUNDING.yml └── workflows │ └── tests.yml ├── .gitignore ├── .yarnrc.yml ├── README.md ├── app ├── controllers │ ├── author.controller.spec.ts │ ├── author.controller.ts │ ├── book.controller.spec.ts │ ├── book.controller.ts │ └── index.ts ├── entities │ ├── Author.ts │ ├── BaseEntity.ts │ ├── Book.ts │ ├── BookTag.ts │ ├── Publisher.ts │ └── index.ts ├── mikro-orm.config.ts └── server.ts ├── docker-compose.yml ├── jest.config.ts ├── package.json ├── renovate.json ├── tsconfig.json └── yarn.lock /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [B4nan] 2 | custom: 'https://paypal.me/mikroorm' 3 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | branches: [ master, renovate/** ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | test: 11 | name: Tests 12 | if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }} 13 | runs-on: ubuntu-latest 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | node-version: [ 18, 20, 22 ] 18 | steps: 19 | - name: Checkout Source code 20 | uses: actions/checkout@v4 21 | 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | 27 | - name: Enable corepack 28 | run: | 29 | corepack enable 30 | corepack prepare yarn@stable --activate 31 | 32 | - name: Init docker 33 | run: docker compose up -d 34 | 35 | - name: Install 36 | run: yarn 37 | 38 | - name: Test 39 | run: yarn test 40 | 41 | - name: Teardown docker 42 | run: docker compose down 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | temp 3 | node_modules 4 | .idea 5 | .yarn/* 6 | !.yarn/patches 7 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Express + MongoDB + TypeScript example integration 2 | 3 | 1. Install dependencies via `yarn` or `npm install` 4 | 2. Run `docker compose up -d` to start mongodb 5 | 3. Run via `yarn start` or `yarn start:dev` (watch mode) 6 | 4. Example API is running on localhost:3000 7 | 8 | Available routes: 9 | 10 | ``` 11 | GET /author finds all authors 12 | GET /author/:id finds author by id 13 | POST /author creates new author 14 | PUT /author/:id updates author by id 15 | ``` 16 | 17 | ``` 18 | GET /book finds all books 19 | GET /book/:id finds book by id 20 | POST /book creates new book 21 | PUT /book/:id updates book by id 22 | ``` 23 | -------------------------------------------------------------------------------- /app/controllers/author.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import expect from 'expect'; 3 | import { app, DI, init } from '../server'; 4 | 5 | describe('author controller', () => { 6 | 7 | beforeAll(async () => { 8 | await init; 9 | DI.orm.config.set('dbName', 'express-test-db'); 10 | DI.orm.config.getLogger().setDebugMode(false); 11 | await DI.orm.config.getDriver().reconnect(); 12 | await DI.orm.getSchemaGenerator().clearDatabase(); 13 | }); 14 | 15 | afterAll(async () => { 16 | await DI.orm.close(true); 17 | DI.server.close(); 18 | }); 19 | 20 | it(`CRUD`, async () => { 21 | let id; 22 | 23 | await request(app) 24 | .post('/author') 25 | .send({ name: 'a1', email: 'e1', books: [{ title: 'b1' }, { title: 'b2' }] }) 26 | .then(res => { 27 | expect(res.status).toBe(200); 28 | expect(res.body.id).toBeDefined(); 29 | expect(res.body.name).toBe('a1'); 30 | expect(res.body.email).toBe('e1'); 31 | expect(res.body.termsAccepted).toBe(false); 32 | expect(res.body.books).toHaveLength(2); 33 | 34 | id = res.body.id; 35 | }); 36 | 37 | await request(app) 38 | .get('/author') 39 | .then(res => { 40 | expect(res.status).toBe(200); 41 | expect(res.body).toHaveLength(1); 42 | expect(res.body[0].id).toBeDefined(); 43 | expect(res.body[0].name).toBe('a1'); 44 | expect(res.body[0].email).toBe('e1'); 45 | expect(res.body[0].termsAccepted).toBe(false); 46 | expect(res.body[0].books).toHaveLength(2); 47 | }); 48 | 49 | await request(app) 50 | .put('/author/' + id) 51 | .send({ name: 'a2' }) 52 | .then(res => { 53 | expect(res.status).toBe(200); 54 | expect(res.body.id).toBeDefined(); 55 | expect(res.body.name).toBe('a2'); 56 | expect(res.body.email).toBe('e1'); 57 | expect(res.body.termsAccepted).toBe(false); 58 | }); 59 | }); 60 | 61 | }); 62 | -------------------------------------------------------------------------------- /app/controllers/author.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import Router from 'express-promise-router'; 3 | import { QueryOrder, wrap } from '@mikro-orm/mongodb'; 4 | 5 | import { DI } from '../server'; 6 | 7 | const router = Router(); 8 | 9 | router.get('/', async (req: Request, res: Response) => { 10 | const authors = await DI.authors.findAll({ 11 | populate: ['books'], 12 | orderBy: { name: QueryOrder.DESC }, 13 | limit: 20, 14 | }); 15 | res.json(authors); 16 | }); 17 | 18 | router.get('/:id', async (req: Request, res: Response) => { 19 | try { 20 | const author = await DI.authors.findOneOrFail(req.params.id, { 21 | populate: ['books'], 22 | }); 23 | 24 | res.json(author); 25 | } catch (e: any) { 26 | return res.status(400).json({ message: e.message }); 27 | } 28 | }); 29 | 30 | router.post('/', async (req: Request, res: Response) => { 31 | if (!req.body.name || !req.body.email) { 32 | res.status(400); 33 | return res.json({ message: 'One of `name, email` is missing' }); 34 | } 35 | 36 | try { 37 | const author = DI.authors.create(req.body); 38 | await DI.em.flush(); 39 | 40 | res.json(author); 41 | } catch (e: any) { 42 | return res.status(400).json({ message: e.message }); 43 | } 44 | }); 45 | 46 | router.put('/:id', async (req: Request, res: Response) => { 47 | try { 48 | const author = await DI.authors.findOneOrFail(req.params.id); 49 | wrap(author).assign(req.body); 50 | await DI.em.flush(); 51 | 52 | res.json(author); 53 | } catch (e: any) { 54 | return res.status(400).json({ message: e.message }); 55 | } 56 | }); 57 | 58 | export const AuthorController = router; 59 | -------------------------------------------------------------------------------- /app/controllers/book.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import expect from 'expect'; 3 | import { init, app, DI } from '../server'; 4 | 5 | describe('book controller', () => { 6 | 7 | beforeAll(async () => { 8 | await init; 9 | DI.orm.config.set('dbName', 'express-test-db'); 10 | DI.orm.config.getLogger().setDebugMode(false); 11 | await DI.orm.config.getDriver().reconnect(); 12 | await DI.orm.getSchemaGenerator().clearDatabase(); 13 | }); 14 | 15 | afterAll(async () => { 16 | await DI.orm.close(true); 17 | DI.server.close(); 18 | }); 19 | 20 | it(`CRUD`, async () => { 21 | let id; 22 | 23 | await request(app) 24 | .post('/book') 25 | .send({ title: 'b1', author: { name: 'a1', email: 'e1' } }) 26 | .then(res => { 27 | expect(res.status).toBe(200); 28 | expect(res.body.id).toBeDefined(); 29 | expect(res.body.title).toBe('b1'); 30 | expect(res.body.author.name).toBe('a1'); 31 | expect(res.body.author.email).toBe('e1'); 32 | expect(res.body.author.termsAccepted).toBe(false); 33 | expect(res.body.author.books).toHaveLength(1); 34 | 35 | id = res.body.id; 36 | }); 37 | 38 | await request(app) 39 | .get('/book') 40 | .then(res => { 41 | expect(res.status).toBe(200); 42 | expect(res.body[0].id).toBeDefined(); 43 | expect(res.body[0].title).toBe('b1'); 44 | expect(res.body[0].author.name).toBe('a1'); 45 | expect(res.body[0].author.email).toBe('e1'); 46 | expect(res.body[0].author.termsAccepted).toBe(false); 47 | }); 48 | 49 | await request(app) 50 | .put('/book/' + id) 51 | .send({ title: 'b2' }) 52 | .then(res => { 53 | expect(res.status).toBe(200); 54 | expect(res.body.id).toBeDefined(); 55 | expect(res.body.title).toBe('b2'); 56 | expect(res.body.author).toBeDefined(); 57 | }); 58 | }); 59 | 60 | }); 61 | -------------------------------------------------------------------------------- /app/controllers/book.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import Router from 'express-promise-router'; 3 | import { QueryOrder, wrap } from '@mikro-orm/mongodb'; 4 | import { DI } from '../server'; 5 | import { Book } from '../entities'; 6 | 7 | const router = Router(); 8 | 9 | router.get('/', async (req: Request, res: Response) => { 10 | const books = await DI.books.findAll({ 11 | populate: ['author'], 12 | orderBy: { title: QueryOrder.DESC }, 13 | limit: 20, 14 | }); 15 | res.json(books); 16 | }); 17 | 18 | router.get('/:id', async (req: Request, res: Response) => { 19 | try { 20 | const book = await DI.books.findOne(req.params.id, { 21 | populate: ['author'], 22 | }); 23 | 24 | if (!book) { 25 | return res.status(404).json({ message: 'Book not found' }); 26 | } 27 | 28 | res.json(book); 29 | } catch (e: any) { 30 | return res.status(400).json({ message: e.message }); 31 | } 32 | }); 33 | 34 | router.post('/', async (req: Request, res: Response) => { 35 | if (!req.body.title || !req.body.author) { 36 | res.status(400); 37 | return res.json({ message: 'One of `title, author` is missing' }); 38 | } 39 | 40 | try { 41 | const book = DI.em.create(Book, req.body); 42 | await DI.em.flush(); 43 | 44 | res.json(book); 45 | } catch (e: any) { 46 | return res.status(400).json({ message: e.message }); 47 | } 48 | }); 49 | 50 | router.put('/:id', async (req: Request, res: Response) => { 51 | try { 52 | const book = await DI.books.findOne(req.params.id); 53 | 54 | if (!book) { 55 | return res.status(404).json({ message: 'Book not found' }); 56 | } 57 | 58 | wrap(book).assign(req.body); 59 | await DI.em.flush(); 60 | 61 | res.json(book); 62 | } catch (e: any) { 63 | return res.status(400).json({ message: e.message }); 64 | } 65 | }); 66 | 67 | export const BookController = router; 68 | -------------------------------------------------------------------------------- /app/controllers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './author.controller'; 2 | export * from './book.controller'; 3 | -------------------------------------------------------------------------------- /app/entities/Author.ts: -------------------------------------------------------------------------------- 1 | import { Cascade, Collection, Entity, OneToMany, Property, ManyToOne } from '@mikro-orm/mongodb'; 2 | import { BaseEntity } from './BaseEntity'; 3 | import { Book } from './Book'; 4 | 5 | @Entity() 6 | export class Author extends BaseEntity { 7 | 8 | @Property() 9 | name: string; 10 | 11 | @Property() 12 | email: string; 13 | 14 | @Property({ nullable: true }) 15 | age?: number; 16 | 17 | @Property() 18 | termsAccepted = false; 19 | 20 | @Property({ nullable: true }) 21 | born?: Date; 22 | 23 | @OneToMany(() => Book, b => b.author, { cascade: [Cascade.ALL] }) 24 | books = new Collection(this); 25 | 26 | @ManyToOne(() => Book, { nullable: true }) 27 | favouriteBook?: Book; 28 | 29 | constructor(name: string, email: string) { 30 | super(); 31 | this.name = name; 32 | this.email = email; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /app/entities/BaseEntity.ts: -------------------------------------------------------------------------------- 1 | import { ObjectId, PrimaryKey, Property, SerializedPrimaryKey } from '@mikro-orm/mongodb'; 2 | 3 | export abstract class BaseEntity { 4 | 5 | @PrimaryKey() 6 | _id!: ObjectId; 7 | 8 | @SerializedPrimaryKey() 9 | id!: string; 10 | 11 | @Property() 12 | createdAt = new Date(); 13 | 14 | @Property({ onUpdate: () => new Date() }) 15 | updatedAt = new Date(); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /app/entities/Book.ts: -------------------------------------------------------------------------------- 1 | import { Cascade, Collection, Entity, ManyToMany, ManyToOne, Property } from '@mikro-orm/mongodb'; 2 | import { Author, BookTag, Publisher } from './index'; 3 | import { BaseEntity } from './BaseEntity'; 4 | 5 | @Entity() 6 | export class Book extends BaseEntity { 7 | 8 | @Property() 9 | title: string; 10 | 11 | @ManyToOne(() => Author) 12 | author: Author; 13 | 14 | @ManyToOne(() => Publisher, { cascade: [Cascade.PERSIST, Cascade.REMOVE], nullable: true }) 15 | publisher?: Publisher; 16 | 17 | @ManyToMany(() => BookTag) 18 | tags = new Collection(this); 19 | 20 | @Property({ nullable: true }) 21 | metaObject?: object; 22 | 23 | @Property({ nullable: true }) 24 | metaArray?: any[]; 25 | 26 | @Property({ nullable: true }) 27 | metaArrayOfStrings?: string[]; 28 | 29 | constructor(title: string, author: Author) { 30 | super(); 31 | this.title = title; 32 | this.author = author; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /app/entities/BookTag.ts: -------------------------------------------------------------------------------- 1 | import { ObjectId, Collection, Entity, ManyToMany, PrimaryKey, Property, SerializedPrimaryKey } from '@mikro-orm/mongodb'; 2 | import { Book } from './Book'; 3 | 4 | @Entity() 5 | export class BookTag { 6 | 7 | @PrimaryKey() 8 | _id!: ObjectId; 9 | 10 | @SerializedPrimaryKey() 11 | id!: string; 12 | 13 | @Property() 14 | name: string; 15 | 16 | @ManyToMany(() => Book, b => b.tags) 17 | books = new Collection(this); 18 | 19 | constructor(name: string) { 20 | this.name = name; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /app/entities/Publisher.ts: -------------------------------------------------------------------------------- 1 | import { ObjectId, Collection, Entity, Enum, OneToMany, PrimaryKey, Property, SerializedPrimaryKey } from '@mikro-orm/mongodb'; 2 | import { Book } from './Book'; 3 | 4 | @Entity() 5 | export class Publisher { 6 | 7 | @PrimaryKey() 8 | _id!: ObjectId; 9 | 10 | @SerializedPrimaryKey() 11 | id!: string; 12 | 13 | @Property() 14 | name: string; 15 | 16 | @Enum(() => PublisherType) 17 | type: PublisherType; 18 | 19 | @OneToMany(() => Book, b => b.publisher) 20 | books = new Collection(this); 21 | 22 | constructor(name: string, type = PublisherType.LOCAL) { 23 | this.name = name; 24 | this.type = type; 25 | } 26 | 27 | } 28 | 29 | export enum PublisherType { 30 | LOCAL = 'local', 31 | GLOBAL = 'global', 32 | } 33 | -------------------------------------------------------------------------------- /app/entities/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Author'; 2 | export * from './Book'; 3 | export * from './BookTag'; 4 | export * from './Publisher'; 5 | export * from './BaseEntity'; 6 | -------------------------------------------------------------------------------- /app/mikro-orm.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@mikro-orm/mongodb'; 2 | import { MongoHighlighter } from '@mikro-orm/mongo-highlighter'; 3 | import { Author, Book, BookTag, Publisher, BaseEntity } from './entities'; 4 | 5 | export default defineConfig({ 6 | entities: [Author, Book, BookTag, Publisher, BaseEntity], 7 | dbName: 'mikro-orm-express-ts', 8 | highlighter: new MongoHighlighter(), 9 | debug: true, 10 | }); 11 | -------------------------------------------------------------------------------- /app/server.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import http from 'http'; 3 | import express from 'express'; 4 | import { EntityManager, EntityRepository, MikroORM, RequestContext } from '@mikro-orm/mongodb'; 5 | 6 | import { AuthorController, BookController } from './controllers'; 7 | import { Author, Book } from './entities'; 8 | import config from './mikro-orm.config'; 9 | 10 | export const DI = {} as { 11 | server: http.Server; 12 | orm: MikroORM, 13 | em: EntityManager, 14 | authors: EntityRepository, 15 | books: EntityRepository, 16 | }; 17 | 18 | export const app = express(); 19 | const port = process.env.PORT || 3000; 20 | 21 | export const init = (async () => { 22 | DI.orm = await MikroORM.init(config); 23 | DI.em = DI.orm.em; 24 | DI.authors = DI.orm.em.getRepository(Author); 25 | DI.books = DI.orm.em.getRepository(Book); 26 | 27 | app.use(express.json()); 28 | app.use((req, res, next) => RequestContext.create(DI.orm.em, next)); 29 | app.get('/', (req, res) => res.json({ message: 'Welcome to MikroORM express TS example, try CRUD on /author and /book endpoints!' })); 30 | app.use('/author', AuthorController); 31 | app.use('/book', BookController); 32 | app.use((req, res) => res.status(404).json({ message: 'No route found' })); 33 | 34 | DI.server = app.listen(port, () => { 35 | console.log(`MikroORM express TS example started at http://localhost:${port}`); 36 | }); 37 | })(); 38 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.4" 2 | 3 | services: 4 | mongo: 5 | container_name: mongo 6 | image: mongo:8.0 7 | ports: 8 | - "27017:27017" 9 | volumes: 10 | - mongo:/data/db 11 | 12 | volumes: 13 | mongo: 14 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '@jest/types'; 2 | 3 | const config: Config.InitialOptions = { 4 | testTimeout: 30000, 5 | preset: 'ts-jest', 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mikro-orm-express-ts-example", 3 | "version": "0.0.1", 4 | "description": "Example integration of MikroORM into express (in typescript)", 5 | "author": "Martin Adamek", 6 | "license": "MIT", 7 | "scripts": { 8 | "start": "tsc && node dist/server", 9 | "start:dev": "tsc-watch --onSuccess \"node dist/server\"", 10 | "start:prod": "tsc && node dist/server", 11 | "test": "jest --runInBand --silent" 12 | }, 13 | "dependencies": { 14 | "@mikro-orm/core": "^6.4.5", 15 | "@mikro-orm/mongo-highlighter": "^1.0.0", 16 | "@mikro-orm/mongodb": "^6.4.5", 17 | "express": "^4.18.2", 18 | "express-promise-router": "^4.1.1", 19 | "tsc-watch": "^7.0.0", 20 | "typescript": "5.8.3" 21 | }, 22 | "devDependencies": { 23 | "@mikro-orm/cli": "^6.4.5", 24 | "@types/express": "^4.17.21", 25 | "@types/express-promise-router": "^3.0.0", 26 | "@types/jest": "29.5.14", 27 | "@types/node": "22.15.30", 28 | "@types/supertest": "^6.0.2", 29 | "jest": "29.7.0", 30 | "supertest": "^7.0.0", 31 | "ts-jest": "^29.1.2", 32 | "ts-node": "^10.9.2" 33 | }, 34 | "mikro-orm": { 35 | "useTsNode": true, 36 | "configPaths": [ 37 | "./app/mikro-orm.config.ts", 38 | "./dist/mikro-orm.config.js" 39 | ] 40 | }, 41 | "packageManager": "yarn@4.9.2" 42 | } 43 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | ":preserveSemverRanges", 5 | ":semanticCommitTypeAll(chore)" 6 | ], 7 | "semanticCommits": "enabled", 8 | "separateMajorMinor": false, 9 | "dependencyDashboard": false, 10 | "packageRules": [ 11 | { 12 | "matchUpdateTypes": [ 13 | "patch", 14 | "minor" 15 | ], 16 | "groupName": "patch/minor dependencies", 17 | "groupSlug": "all-non-major", 18 | "automerge": true, 19 | "automergeType": "branch" 20 | } 21 | ], 22 | "schedule": [ 23 | "every weekday" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "NodeNext", 4 | "target": "ESNext", 5 | "moduleResolution": "NodeNext", 6 | "declaration": true, 7 | "outDir": "./dist", 8 | "esModuleInterop": true, 9 | "strict": true, 10 | "experimentalDecorators": true, 11 | "emitDecoratorMetadata": true 12 | }, 13 | "include": ["./app"], 14 | "exclude": ["node_modules"] 15 | } 16 | --------------------------------------------------------------------------------