Getting Started

Pearl is a TypeScript framework for Node.js that ships everything you need in one install: routing, JWT auth, Drizzle ORM, request validation, BullMQ queues, typed events, and mail — all pre-integrated, zero glue required.

Requirements

  • Node.js v20 or higher
  • TypeScript v5.4 or higher
  • PostgreSQL, MySQL, or SQLite (for database features)
  • Redis (only needed for queue features)

Scaffold a new project

The fastest way to get started. One command creates a fully structured project, installs dependencies, and generates a .env file for you.

···
npx @pearl-framework/cli new my-api
cd my-api
npm run dev

Your server is now running at http://localhost:3000. That's it.

Manual install

Prefer to wire things up yourself? Install the meta-package — it includes every Pearl sub-package in one go:

···
npm install @pearl-framework/pearl drizzle-orm zod dotenv

Project structure

After scaffolding, your project looks like this:

···
my-api/
├── src/
   ├── controllers/       # HTTP route handlers
   ├── events/            # Domain events
   ├── jobs/              # Background jobs
   ├── listeners/         # Event listeners
   ├── mail/              # Mailable email classes
   ├── middleware/        # Custom middleware
   ├── schema/            # Drizzle table definitions
   ├── providers/
   └── AppServiceProvider.ts
   ├── requests/          # FormRequest validation
   └── server.ts          # Entry point
├── database/
   └── migrations/
├── .env                   # Auto-created and auto-loaded on boot
├── .env.example
├── package.json
└── tsconfig.json

The entry point

src/server.ts is generated for you by the scaffold. Two important things to understand before you change anything:

  • import 'dotenv/config' is included at the top of the generated server.ts — this loads your .env before anything else runs.
  • root: import.meta.dirname tells Pearl where your project root is so it can find config files. Don't remove it.
src/server.ts
import 'dotenv/config'
import { Application, HttpKernel, Router } from '@pearl-framework/pearl'
import { AppServiceProvider } from './providers/AppServiceProvider.js'

const app = new Application({ root: import.meta.dirname })
app.register(AppServiceProvider)
await app.boot()  // all providers booted

const router = new Router()

router.get('/', (ctx) =>
  ctx.response.json({ message: 'Welcome to Pearl 🦪' })
)

await new HttpKernel()
  .useRouter(router)
  .listen(Number(process.env.PORT ?? 3000))

console.log('🦪 Pearl running on http://localhost:3000')

Service providers

A service provider is where you register services into Pearl's IoC container — your database, auth guards, queue manager, and anything else your app needs. The scaffold generates one at src/providers/AppServiceProvider.ts.

Each provider has two lifecycle methods:

  • register() — bind services into the container. Must be synchronous. Do not call make() here — other providers may not have registered yet.
  • boot() — called after all providers are registered and .env is loaded. Safe for async work like opening DB connections.
src/providers/AppServiceProvider.ts
import { ServiceProvider, DatabaseManager } from '@pearl-framework/pearl'
import { DrizzleAdapter } from '@pearl-framework/database'

export class AppServiceProvider extends ServiceProvider {
  register(): void {
    // Bind DatabaseManager as a singleton — constructed once, reused everywhere
    this.container.singleton(DatabaseManager, () =>
      new DatabaseManager(new DrizzleAdapter({
        driver:   'postgres',
        host:     process.env.DB_HOST     ?? 'localhost',
        port:     Number(process.env.DB_PORT ?? 5432),
        user:     process.env.DB_USER     ?? 'postgres',
        password: process.env.DB_PASSWORD ?? '',
        database: process.env.DB_NAME     ?? 'my_api',
      }))
    )
  }

  override async boot(): Promise<void> {
    // .env is loaded by this point — safe to read env vars and connect
    await this.container.make(DatabaseManager).connect()
  }
}

Environment variables

The scaffold creates a .env from .env.example. Edit it before starting the server:

.env
APP_NAME=my-api
PORT=3000

# Database
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=secret
DB_NAME=my_api

# Redis — only needed if you use queues
REDIS_HOST=localhost
REDIS_PORT=6379

# Generate with: openssl rand -base64 32
JWT_SECRET=change-this-in-production

Next steps