| name | minimajs |
| description | Comprehensive guide for building backends with minimajs — a TypeScript-first HTTP framework for Node.js and Bun. Use this skill whenever the user is working with minimajs, imports from @minimajs/*, asks about routing, validation, auth, file uploads, OpenAPI docs, storage, cookies, hooks, plugins, or testing in a minimajs project. Also trigger when the user wants to create a new minimajs app from scratch or extend an existing one.
|
minimajs
minimajs is a TypeScript-first, ESM-only HTTP framework for Node.js and Bun. Its central idea: request context lives in AsyncLocalStorage, so you never thread req/res through function calls — just import and call context helpers from anywhere in the request scope.
Scaffolding a new project
npx @minimajs/cli new hello-world
bunx @minimajs/cli new hello-world
npx @minimajs/cli new hello-world --pm pnpm
npx @minimajs/cli new hello-world --no-install
npx @minimajs/cli new hello-world --no-git
The scaffolded project includes ./app — a shell script entry point for all CLI commands.
CLI
minimajs ships a CLI accessed via the ./app shell script scaffolded into every project.
Dev workflow
./app dev
./app dev --no-run
./app build
./app start
Configuration — minimajs.config.ts
import { defineConfig } from "@minimajs/cli";
export default defineConfig(({ dev, mode }) => ({
envFile: ".env",
sourcemap: dev,
}));
defineConfig accepts a plain config object or a factory receiving { mode, dev } — where mode is "dev" | "build" | "start" and dev is shorthand for mode === "dev".
CLI Plugins — definePlugins
Plugins are registered separately from build config via a named plugins export. They can extend the CLI with new commands (root-level) and generators (under ./app add).
import { defineConfig, definePlugins } from "@minimajs/cli";
import { queuePlugin } from "@myapp/queue-plugin";
export default defineConfig(({ dev }) => ({ sourcemap: dev }));
export const plugins = definePlugins([queuePlugin()]);
export const plugins = definePlugins(({ dev }) => [queuePlugin({ verbose: dev })]);
Writing a plugin
import { defineCommand } from "@minimajs/cli";
import type { CliPlugin } from "@minimajs/cli";
export const queuePlugin = (): CliPlugin => ({
name: "queue",
commands: {
queue: defineCommand({
meta: { description: "Manage the job queue" },
subCommands: {
flush: defineCommand({
meta: { description: "Flush all pending jobs" },
async run() {
},
}),
},
}),
},
generators: {
job: defineCommand({
meta: { description: "Scaffold a queue job" },
args: { name: { type: "positional", description: "Job name" } },
async run({ args }) {
},
}),
},
});
Plugin commands appear at the root of ./app — not under a command namespace. Generator commands appear under ./app add <generator-name>.
Generators — ./app add <type> <name>
All generators create a file and auto-patch the nearest module.ts to register it.
Module
./app add module orders
./app add module api/orders
Hook
./app add hook request
./app add hook orders/request
./app add hook request --type send
./app add hook request --global
Plugin
./app add plugin hello
./app add plugin orders/hello
Plugin export name is toCamelCase(name) — e.g. hello → export const hello: Plugin.
Middleware
Middleware is always global — always registered in root src/module.ts.
./app add middleware request
./app add middleware auth/jwt
Export name is toCamelCase(name) — e.g. request → export const request = middleware(...).
Service
./app add service users
Disk
./app add disk
./app add disk uploads
./app add disk primary --driver aws-s3
./app add disk cdn --driver azure-blob
./app add disk router --proto
App creation
import { logger } from "@minimajs/server";
import { createApp } from "@minimajs/server/bun";
import { createApp } from "@minimajs/server/node";
const app = createApp();
app.get("/", () => ({ message: "Hello" }));
const addr = await app.listen({ port: 3000 });
logger.info("App started %s", addr);
createApp(options?) options:
prefix — URL prefix for all routes
logger — Pino logger instance, or false to disable
moduleDiscovery — disable file-based module discovery (useful in tests)
Two routing styles
1. File-based modules (preferred)
Directory structure maps to URL prefixes. Each module.ts registers routes for its subtree.
src/
├── index.ts ← createApp() + listen
├── module.ts ← root: global plugins, /api prefix
├── users/
│ └── module.ts ← /api/users/*
└── posts/
└── module.ts ← /api/posts/*
A module file can export:
routes — declarative route map
meta — prefix + plugins scoped to this module
default function(app) — programmatic registration
import type { Meta, Routes } from "@minimajs/server";
export const meta: Meta = {
plugins: [
],
};
export const routes: Routes = {
"GET /": listUsers,
"POST /": createUser,
"GET /:id": getUser,
"DELETE /:id": deleteUser,
};
Route params: :id, :id? (optional), * (wildcard), :id(\\d+) (regex).
Context API — core helpers
All imported from @minimajs/server. Call from any function within a request.
import {
body,
params,
headers,
searchParams,
request,
response,
abort,
redirect,
defer,
onError,
context,
createContext,
} from "@minimajs/server";
const data = body<{ name: string }>();
const id = params.get("id");
const maybeId = params.optional("id");
const all = params<{ id: string }>();
const page = searchParams.get("page");
const all = searchParams<{ page: string; limit: string }>();
const token = headers.get("authorization");
const all = headers<{ authorization: string }>();
headers.set("x-request-id", "abc123");
headers.set({ "x-foo": "bar", "x-baz": "qux" });
response({ id: 1 });
response.status(201);
function getUser() {
return { id: 1, name: "Alice" };
}
const req = request();
const url = request.url();
const signal = request.signal();
const ip = request.ip();
defer(() => analytics.track("request"));
onError((err) => logger.error(err));
Error handling
import { abort, redirect } from "@minimajs/server";
abort("Not found", 404);
abort.notFound("User not found");
abort.is(err);
abort.rethrow(err);
redirect("/login");
redirect("/new-path", true);
Override error response shape globally (do this at app startup):
import { HttpError } from "@minimajs/server/error";
import { ValidationError } from "@minimajs/schema";
HttpError.toJSON = (err) => ({
success: false,
message: err.response,
statusCode: err.status,
});
ValidationError.toJSON = (err) => ({
success: false,
error: "Validation failed",
issues: err.issues?.map((i) => ({ field: i.path.join("."), message: i.message })),
});
Package selection guide
| Need | Package | Key import |
|---|
| Validate request body/params/query | @minimajs/schema | createBody, createParams, createSearchParams |
| Authenticate users | @minimajs/auth | createAuth |
| File uploads | @minimajs/multipart | multipart, createMultipart |
| File storage (FS/S3/Azure) | @minimajs/disk | createDisk, createProtoDisk |
| OpenAPI docs | @minimajs/openapi | openapi, describe, schema |
| Cookies | @minimajs/cookie | cookies |
| CORS | @minimajs/server | cors (built-in plugin) |
| Graceful shutdown | @minimajs/server | shutdown (built-in plugin) |
| Proxy/IP extraction | @minimajs/server | proxy (built-in plugin) |
Common patterns
Validation + OpenAPI
import { createBody, createParams, createResponse, schema } from "@minimajs/schema";
import { describe } from "@minimajs/openapi";
import { handler } from "@minimajs/server";
import { z } from "zod";
const getBody = createBody(z.object({ name: z.string(), email: z.string().email() }));
const userResponse = createResponse(z.object({ id: z.string(), name: z.string() }));
export const routes: Routes = {
"POST /": handler(describe({ summary: "Create user", tags: ["Users"] }), schema(getBody, userResponse), async () => {
const { name, email } = getBody();
return { id: crypto.randomUUID(), name };
}),
};
Authentication
import { createAuth } from "@minimajs/auth";
import { UnauthorizedError } from "@minimajs/auth";
import { headers } from "@minimajs/server";
export const [authPlugin, getUser] = createAuth(async () => {
const token = headers.get("authorization")?.replace("Bearer ", "");
if (!token) throw new UnauthorizedError();
return await verifyToken(token);
});
export const meta: Meta = { plugins: [authPlugin] };
export const routes: Routes = {
"GET /profile": () => {
const user = getUser.required();
return { name: user.name };
},
};
File upload
import { createMultipart } from "@minimajs/multipart/schema";
import { helpers } from "@minimajs/multipart";
import { z } from "zod";
const upload = createMultipart({
name: z.string().min(1),
avatar: z
.file()
.max(5 * 1024 * 1024)
.mime(["image/jpeg", "image/png"]),
});
export const routes: Routes = {
"POST /upload": async () => {
const { name, avatar } = await upload();
const filename = await helpers.save(avatar, "./public/avatars");
return { name, avatar: `/avatars/${filename}` };
},
};
Global plugins (CORS, OpenAPI, shutdown)
import { cors, shutdown } from "@minimajs/server/plugins";
import { openapi } from "@minimajs/openapi";
export const meta: Meta = {
prefix: "/api",
plugins: [cors({ origin: "*" }), shutdown(), openapi({ info: { title: "My API", version: "1.0.0" } })],
};
Custom request-scoped context
import { createContext, plugin, hook } from "@minimajs/server";
const [getRequestId, setRequestId] = createContext<string>();
export const requestIdPlugin = plugin.sync((app) => {
app.register(hook("request", () => setRequestId(crypto.randomUUID())));
app.register(
hook("send", (res, ctx) => {
ctx.responseState.headers.set("x-request-id", getRequestId() ?? "");
})
);
});
export { getRequestId };
Database lifecycle
import { hook } from "@minimajs/server";
export const meta: Meta = {
plugins: [
hook.lifespan(async () => {
await db.connect();
return async () => db.disconnect();
}),
],
};
Reference files
For deeper API details, read the relevant reference file:
- references/cli-plugins.md — full
CliPlugin shape, esbuild hooks, CLI commands, generators, publishing guide
- references/server.md — full hook lifecycle, plugin system, built-in plugins, module discovery config
- references/schema.md — all
createBody/createParams/createSearchParams/createHeaders options, async variants, response schemas
- references/auth.md —
createAuth options, required mode, UnauthorizedError/ForbiddenError
- references/multipart.md —
raw/streaming namespaces, helpers API, limits config
- references/disk.md — full Disk API, ProtoDisk multi-provider routing, S3/Azure drivers, disk hooks
- references/openapi.md —
describe(), internal(), generateOpenAPIDocument(), security schemes
- references/testing.md —
app.handle(), createRequest(), mocking context
- references/cookie.md —
cookies(), cookies.get/set/remove(), set options, type-safe cookies