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
- 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
- Encryption & hashing:
Crypt,Hash, signed URLs
Infrastructure
- Cache: stores, locks, rate limiting
- Queues: jobs, workers, retries, chaining
- Scheduling: recurring tasks
- Events: dispatching and listening
- Broadcasting: websockets
- Storage: file disks
- Mail: mailables and transports
- Notifications: multi-channel notifications
- 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/cache | Cache stores, locks, rate limiter |
@mahiframework/queue | Jobs, queue drivers, workers, middleware |
@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/mail | Mailables, SMTP/log transports |
@mahiframework/notifications | Multi-channel notifications |
@mahiframework/encryption | Encrypter, hasher, signer |
@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/snowflake | Distributed 63-bit IDs |
@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