Part 1 - Setting Up NestJS with ESM, Vite and TypeORM
In this part, we'll set up a new NestJS project and configure everything needed to get started.
First, create a new NestJS project by following the NestJS First Steps guide.
Then install the required DugongJS packages:
npm install @dugongjs/core @dugongjs/nestjs
Setting Up ESM with Vite
DugongJS is built for native ECMAScript Modules (ESM), but NestJS is configured for CommonJS (CJS) by default. There are several ways to configure NestJS with ESM. In this tutorial, we'll be using Vite (and ViteNode in development). If you have another preferred way of setting up ESM, feel free to skip this part.
Install the required dev dependencies:
npm install --save-dev vite vite-node vite-plugin-node dotenv-cli
Then create a vite.config.ts
file:
import { defineConfig } from "vite";
import { VitePluginNode } from "vite-plugin-node";
export default defineConfig({
build: {
ssr: true,
outDir: "./dist"
},
plugins: [
...VitePluginNode({
adapter: "nest",
appPath: "./src/main.ts",
tsCompiler: "swc",
outputFormat: "esm",
swcOptions: {
minify: false
}
})
]
});
Next, in package.json,
set the type
to module
to declare it an ESM module and update the scripts to use vite
for production build and vite-node
for development.
{
"type": "module",
"scripts": {
"build": "vite build",
"start:dev": "dotenv -e .env -- vite-node src/main.ts"
}
}
Finally, update your tsconfig.json
to support ESM:
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext"
}
}
When using NodeNext
module resolution, TypeScript requires all file imports to end in .js — even when importing TypeScript files. This will cause all your existing imports to error if they use the default module resolution. Learn more.
Verify that everything has been set up correctly by running the following script:
npm run start:dev
You should see the normal log output from NestJS.
Also verify the build script:
npm run build
And make sure the dist
folder contains a main.js
file.
Installing TypeORM and PostgreSQL
We’ll use TypeORM for persistence and configure it with PostgreSQL.
Install the following dependencies:
npm install typeorm @nestjs/typeorm @dugongjs/typeorm @dugongjs/nestjs-typeorm
To keep things organized, we’ll store our database configuration in a dedicated folder. Add the following to your project:
📁 src
└─ 📁 db
│ └─ 📄 data-source-options.ts
Create a data source configuration file:
import { ConsumedMessageEntity, DomainEventEntity, SnapshotEntity } from "@dugongjs/typeorm";
import type { DataSourceOptions } from "typeorm";
export const dataSourceOptions: DataSourceOptions = {
type: "postgres",
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
synchronize: true,
logging: false,
entities: [DomainEventEntity, SnapshotEntity, ConsumedMessageEntity]
};
Setting synchronize: true
automatically generates tables based on your entities. This is useful during development, but should be disabled in production environments in favor of migrations.
In this tutorial, we're just using process.env
to access environmental variables, but you could also use the ConfigModule
from @nestjs/config
for that.
Create a .env
file at the root of your project with your database settings:
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=postgres_user
DB_PASSWORD=postgres_password
DB_NAME=account_service_db
Setting Up PostgreSQL with Docker Compose
If you don’t already have a PostgreSQL instance, you can spin one up with Docker. Create a docker-compose.yaml
file:
services:
postgres:
image: postgres:14
container_name: nestjs_tutorial_account_service_db
restart: unless-stopped
environment:
POSTGRES_USER: postgres_user
POSTGRES_PASSWORD: postgres_password
POSTGRES_DB: account_service_db
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
Start the container:
docker compose up
Configuring the App Module
In src/app.module.ts
, connect TypeORM, the DugongJS adapters, and set the current origin for event publishing:
import { EventIssuerModule } from "@dugongjs/nestjs";
import { RepositoryTypeOrmModule, TransactionManagerTypeOrmModule } from "@dugongjs/nestjs-typeorm";
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { dataSourceOptions } from "./db/data-source-options.js";
@Module({
imports: [
TypeOrmModule.forRoot(dataSourceOptions),
RepositoryTypeOrmModule.forRoot(),
TransactionManagerTypeOrmModule.forRoot(),
EventIssuerModule.forRoot({ currentOrigin: "BankingContext-AccountService" })
]
})
export class AppModule {}
Let’s break down what each module does:
TypeOrmModule.forRoot()
sets up TypeORM using our previously defined config.RepositoryTypeOrmModule
provides adapters for the DugongJS repository ports.TransactionManagerTypeOrmModule
provides an adapter for the DugongJS transaction manager port.EventIssuerModule
configures thecurrentOrigin
— a label that identifies which service owns the aggregates and emits domain events. See origin for more details.
In the next part, we’ll implement the domain layer, including the aggregate, domain events, and commands for our bank account model.