Mahi
A TypeScript application framework for building APIs and services on Node.js.
Mahi takes the architecture that makes Laravel productive, service providers with a two-stage lifecycle, a service container, driver-based managers, an expressive ORM, first-class queues and scheduling, and rebuilds it for TypeScript, where the type system does work that PHP's runtime magic had to do at runtime.
export class PostsServiceProvider extends ServiceProvider {
routes(router: Router): void {
router.get("/posts/{post}", ShowPostController).name("posts.show");
}
}
export class ShowPostController extends Controller {
async handle(request: Request) {
const post = await request.model(Post);
await post.load("author", "comments");
return HttpResponse.json(await new PostResource(post).toJson());
}
}
Getting started
npm create mahi@latest my-app
cd my-app
./artisan serve
See the installation guide for the full walkthrough.
Design principles
These are the decisions that shape everything else, and the places Mahi deliberately diverges from Laravel.
Explicit resolution, no decorators. There is no reflect-metadata, no
@Injectable, no constructor auto-wiring. A service provider binds a
factory; consumers call app.make(TOKEN). Resolution is a function call
you can read and follow, and the container never has to guess what a
constructor parameter means.
Types are the documentation. data_get(user, "profile.city") is
checked against user's shape at compile time and returns the type at
that path. request.validated() returns a type derived from the rules you
declared. A model's with("author") narrows the result type to include
the loaded relation. Where Laravel returns mixed and asks you to know,
Mahi returns a type.
No dynamic facade proxies. Laravel's facades forward arbitrary method names at runtime, which no type checker can follow. Mahi's facades are hand-written classes with real static methods that each proxy exactly one token, so renaming an underlying method is a compile error, not a runtime surprise.
Synchronous driver resolution. manager.driver() never returns a
promise. Constructing a driver handle is cheap; real I/O is lazy. Drivers
that genuinely need async setup implement Connectable and are connected
by their owning provider's boot().
Escape hatches are first-class. Every abstraction exposes the layer
below it: builder.toBase() for the query builder, .raw() for the
underlying Kysely query, Expression.raw() for literal SQL,
request.raw() for the Hono context. You should never have to fight the
framework to do something it didn't anticipate.
Documentation
Getting started
- Installation: creating and running a new application
- Configuration: config files, environment variables
- Application lifecycle: bootstrap, the two-stage boot
- Deployment: running in production
Core concepts
- Service container: binding and resolving services
- Service providers: the extension point for everything
- Helpers:
Str,Arr,Collection,Number,data_get
The HTTP layer
- Routing: routes, groups, middleware, named routes, URLs
- Requests: input, files, form requests
- Validation: rules, custom messages, typed output
- Controllers: single-action controllers
- Responses: JSON, files, redirects, API resources
Database
- Getting started: connections, the query builder, transactions
- Models: attributes, casts, events, serialization
- Extending package models: subclassing and swapping a package's models
- Relationships: defining and eager-loading relations
- Queries: the fluent query builder in depth
- Migrations: schema, seeders, factories
- Pagination: length-aware, simple, and cursor paginators
Security
- Authentication: guards, tokens, sessions, passwords
- Authorization: gates, policies, abilities
- Permissions: roles, permissions, cached checks
- Multi-factor auth: TOTP, email codes, recovery codes, step-up
- Impersonation: acting as another user, gated
- Socialite: OAuth sign-in with GitHub, Google, OIDC and more
- Encryption & hashing:
Crypt,Hash, signed URLs - Activity logs: resource, security and custom activity records
Infrastructure
- Cache: stores, locks, rate limiting
- Queues: jobs, workers, retries, chaining
- Watchtower: supervised workers, run history, dashboard
- Scheduling: recurring tasks
- Events: dispatching and listening
- Broadcasting: websockets
- Storage: file disks
- Media: uploads, collections, image modifiers, archives
- Mail: mailables and transports
- Notifications: multi-channel notifications
- Settings: database-persisted, cached global settings
- Health checks: readiness probes,
/health,./artisan health - Logging: channels and stacks
- Redis: the multi-process story
- HTTP client: outbound requests, retries, fakes
Tooling
- Console:
artisan, writing commands - Testing: the test application, fakes, assertions
- Dates & times: the
DateTimeAPI
Packages
Mahi is a set of packages, not a monolith. Install what you use.
| Package | Contents |
|---|---|
@mahiframework/core | Container, Application, ServiceProvider, Config, Env, Logger, Str/Arr/Collection, helpers |
@mahiframework/http | HTTP kernel (Hono), router, request, responses, resources, middleware |
@mahiframework/database | Models, query builder, relations, migrations, factories, seeders |
@mahiframework/validation | Rule, Validator, ValidationException |
@mahiframework/auth | Guards (token, session), user providers, password reset, verification |
@mahiframework/authorization | Gates, policies, abilities |
@mahiframework/permissions | Roles, permissions, cached authorization checks |
@mahiframework/impersonation | Impersonation gate, start/stop, nested chains |
@mahiframework/socialite | OAuth providers, redirect/callback, user mapping |
@mahiframework/socialite-oidc | A generic OpenID Connect driver, with full id_token validation |
@mahiframework/cache | Cache stores, locks, rate limiter |
@mahiframework/queue | Jobs, queue drivers, workers, middleware |
@mahiframework/watchtower | Supervised worker pool, run history, queue dashboard |
@mahiframework/schedule | Recurring task scheduling |
@mahiframework/health | Readiness checks, GET /health, ./artisan health |
@mahiframework/events | Event dispatcher, listeners |
@mahiframework/broadcasting | Websocket broadcasting |
@mahiframework/storage | Filesystem disks |
@mahiframework/storage-ftp | An FTP disk, for files on a legacy or appliance host |
@mahiframework/storage-s3 | An S3 disk, for files in object storage |
@mahiframework/storage-sftp | An SFTP disk, for files on a remote host |
@mahiframework/media | Uploads, collections, image modifiers, streaming archives |
@mahiframework/media-sharp | A sharp/libvips image driver, so image modifiers do something |
@mahiframework/mail | Mailables, SMTP/log transports |
@mahiframework/notifications | Multi-channel notifications |
@mahiframework/encryption | Encrypter, hasher, signer |
@mahiframework/activity-logs | Resource/security/custom activity records |
@mahiframework/settings | Database-persisted, cached global application settings |
@mahiframework/redis | Redis-backed cache/queue/broadcast drivers |
@mahiframework/cli | Console kernel, make:* generators, migration commands |
@mahiframework/testing | Test application, HTTP client, database assertions |
@mahiframework/datetime | Immutable date/time library |
@mahiframework/tui | Terminal UI: prompts, tables, spinners, progress bars |
@mahiframework/pipeline | Send a value through a series of pipes |
@mahiframework/process | Run external commands |
@mahiframework/http-client | Outbound HTTP: fluent requests, retries, Http.fake() |
@mahiframework/facades | The Facade<T> mixin |
Requirements
- Node.js 22 or later
- No PHP, no compiled extensions beyond
better-sqlite3andargon2