v1.0.0  ·  11 packages  ·  now on npm

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
src/server.ts
// .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.

@pearl/core1.0.0

IoC container, application kernel, service providers

@pearl/http1.0.0

HTTP kernel — router, middleware pipeline, request/response

@pearl/auth1.0.0

JWT, session, and API token authentication guards

@pearl/database1.0.0

Drizzle ORM — Postgres, MySQL, SQLite integration

@pearl/validate1.0.0

Zod-powered FormRequest, validation pipes, error formatting

@pearl/events1.0.0

Type-safe event dispatcher, listeners, queued events

@pearl/queue1.0.0

BullMQ-powered job dispatching, workers, and retries

@pearl/mail1.0.0

Nodemailer-powered mailable classes, transports, queue support

@pearl/testing1.0.0

HTTP test client, database helpers, mail fakes, test utilities

@pearl/cli1.0.0

CLI for scaffolding — new, serve, make:*

@pearl/pearl1.0.0

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 docs
routes/api.ts
import { 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)
})

// ProtectedAuthenticate() 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 docs
schema/posts.ts
import { 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 migrate

Validation

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 docs
requests/CreatePostRequest.ts
import { 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 docs
jobs/SendWelcomeEmailJob.ts
import { 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
~ npx pearl new my-api
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

Start building

Your next API starts
with one command.

npx pearl new my-api scaffolds everything. pearl serve and you're live.

Read the docs View on GitHub