Build TypeScript APIs
without the glue work.
Pearl is a batteries-included Node.js framework — routing, JWT auth, Drizzle ORM, Zod validation, BullMQ queues, typed events, and mail. All wired together. One install.
Install
npm install @pearl-framework/pearl// .env is created by `pearl new` and loaded inside
// app.boot() automatically — no dotenv import needed
import { Application, HttpKernel, Router }
from '@pearl-framework/pearl'
import { AuthManager, Authenticate }
from '@pearl-framework/pearl'
import { AppServiceProvider }
from './providers/AppServiceProvider.js'
const app = new Application({ root: import.meta.dirname })
app.register(AppServiceProvider)
await app.boot() // loads .env + boots all providers
const auth = app.container.make(AuthManager)
const router = new Router()
// Public route
router.get('/health', ctx =>
ctx.json({ status: 'ok' })
)
// Protected — Bearer token required
router.get('/me', ctx => ctx.json(ctx.get('auth.user')),
[Authenticate(auth)]
)
await new HttpKernel()
.useRouter(router)
.listen(3000)What you get
11 packages. One install.
@pearl-framework/pearl is a meta-package that pulls in all 11 below. Each is also available individually if you prefer à la carte.
IoC container, application kernel, service providers
HTTP kernel — router, middleware pipeline, request/response
JWT, session, and API token authentication guards
Drizzle ORM — Postgres, MySQL, SQLite integration
Zod-powered FormRequest, validation pipes, error formatting
Type-safe event dispatcher, listeners, queued events
BullMQ-powered job dispatching, workers, and retries
Nodemailer-powered mailable classes, transports, queue support
HTTP test client, database helpers, mail fakes, test utilities
CLI for scaffolding — new, serve, make:*
Meta-package — installs all packages in one command
Routing & Middleware
Routes. Middleware.Typed end-to-end.
Define routes with a fully-typed HttpContext — params, query, body, and the authenticated user, all inferred. Apply middleware with a single array: authentication, logging, rate-limiting. No decorators, no magic.
Read the docsimport { Router } from '@pearl-framework/http'
import { Authenticate } from '@pearl-framework/auth'
const router = new Router()
// Public
router.get('/health', ctx =>
ctx.json({ status: 'ok', ts: Date.now() })
)
// Params, query, body — all typed
router.get('/posts/:id', async ctx => {
const id = ctx.param('id')
const page = ctx.query('page') ?? '1'
const post = await db.query.posts
.findFirst({ where: eq(posts.id, Number(id)) })
return post
? ctx.json(post)
: ctx.json({ error: 'Not found' }, 404)
})
// Protected — Authenticate() is a typed middleware
router.post('/posts', createPost, [Authenticate(auth)])
router.put('/posts/:id', updatePost, [Authenticate(auth)])Database
Drizzle ORM.Built right in.
@pearl-framework/database wires Drizzle directly into the IoC container. Define your schema in TypeScript, query with full autocomplete and type safety, and run migrations with pearl migrate. Supports Postgres, MySQL, and SQLite.
Read the docsimport { pgTable, serial, text, integer, timestamp } from 'drizzle-orm/pg-core'
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
content: text('content').notNull(),
authorId: integer('author_id').references(() => users.id),
status: text('status', { enum: ['draft','published'] })
.notNull().default('draft'),
createdAt: timestamp('created_at').defaultNow(),
})
// Fully typed, full autocomplete
const post = await db.query.posts.findFirst({
where: eq(posts.id, Number(ctx.param('id'))),
with: { author: true },
})
// $ pearl migrateValidation
Zod validation.Before you see it.
Extend FormRequest with a Zod schema. Pearl validates the incoming body before your controller runs — invalid requests get a structured 422 with field-level errors automatically. Nothing leaks through.
Read the docsimport { FormRequest } from '@pearl-framework/validate'
import { z } from 'zod'
export class CreatePostRequest extends FormRequest {
schema = z.object({
title: z.string().min(3).max(120),
content: z.string().min(10),
tags: z.array(z.string()).max(5).optional(),
status: z.enum(['draft', 'published']).default('draft'),
})
}
async function createPost(ctx: HttpContext) {
const data = await CreatePostRequest.validate(ctx)
// data is fully typed: { title: string, content: string, ... }
const [post] = await db.insert(posts).values(data).returning()
return ctx.json(post, 201)
}
// Bad input auto-rejected:
// HTTP 422 { errors: { title: ['Too short'] } }Queues & Events
Background jobs.Decoupled events.
Dispatch slow work to BullMQ Redis workers with one line. Decouple side-effects using typed domain events — fire them from your service, react in dedicated listener classes. No direct imports between layers.
Read the docsimport { Job } from '@pearl-framework/queue'
import { Event, Listen, emit } from '@pearl-framework/events'
class SendWelcomeEmailJob extends Job {
userId!: number
async handle() {
await Mail.send(new WelcomeMail(this.userId))
}
}
class UserRegisteredEvent extends Event {
constructor(public user: User) { super() }
}
// Fire from your service
await emit(new UserRegisteredEvent(user))
@Listen(UserRegisteredEvent)
class OnUserRegistered {
async handle({ user }: UserRegisteredEvent) {
await Queue.dispatch(
Object.assign(new SendWelcomeEmailJob(), { userId: user.id })
)
}
}CLI scaffolding
One command.
Everything generated.
npx pearl new my-api gives you a complete, structured project. .env is created automatically and loaded by Pearl on boot — no dotenv import, no manual wiring.
- .env auto-created and loaded on boot — no import needed
- IoC container with constructor injection throughout
- pearl make:controller, model, job, event, listener, migration
- pearl serve — hot-reload dev server, zero config
- pearl migrate — Drizzle migrations from the CLI
my-api/
├── src/
│ ├── controllers/ # HTTP handlers
│ ├── schema/ # Drizzle table defs
│ ├── middleware/ # custom middleware
│ ├── jobs/ # BullMQ background jobs
│ ├── events/ # domain events
│ ├── listeners/ # event listeners
│ ├── mail/ # Mailable classes
│ ├── requests/ # Zod FormRequest validation
│ ├── routes/api.ts # all your routes
│ ├── database/migrations/
│ ├── providers/ # service providers
│ └── server.ts # entry point
├── .env # auto-created & auto-loaded ✓
├── package.json
└── tsconfig.json