This commit is contained in:
bipproduction
2026-04-01 10:43:03 +08:00
parent 816db7568c
commit 39d659acd0
175 changed files with 21765 additions and 0 deletions

View File

@@ -0,0 +1,175 @@
---
name: better-auth-best-practices
description: Configure Better Auth server and client, set up database adapters, manage sessions, add plugins, and handle environment variables. Use when users mention Better Auth, betterauth, auth.ts, or need to set up TypeScript authentication with email/password, OAuth, or plugin configuration.
---
# Better Auth Integration Guide
**Always consult [better-auth.com/docs](https://better-auth.com/docs) for code examples and latest API.**
---
## Setup Workflow
1. Install: `npm install better-auth`
2. Set env vars: `BETTER_AUTH_SECRET` and `BETTER_AUTH_URL`
3. Create `auth.ts` with database + config
4. Create route handler for your framework
5. Run `npx @better-auth/cli@latest migrate`
6. Verify: call `GET /api/auth/ok` — should return `{ status: "ok" }`
---
## Quick Reference
### Environment Variables
- `BETTER_AUTH_SECRET` - Encryption secret (min 32 chars). Generate: `openssl rand -base64 32`
- `BETTER_AUTH_URL` - Base URL (e.g., `https://example.com`)
Only define `baseURL`/`secret` in config if env vars are NOT set.
### File Location
CLI looks for `auth.ts` in: `./`, `./lib`, `./utils`, or under `./src`. Use `--config` for custom path.
### CLI Commands
- `npx @better-auth/cli@latest migrate` - Apply schema (built-in adapter)
- `npx @better-auth/cli@latest generate` - Generate schema for Prisma/Drizzle
- `npx @better-auth/cli mcp --cursor` - Add MCP to AI tools
**Re-run after adding/changing plugins.**
---
## Core Config Options
| Option | Notes |
|--------|-------|
| `appName` | Optional display name |
| `baseURL` | Only if `BETTER_AUTH_URL` not set |
| `basePath` | Default `/api/auth`. Set `/` for root. |
| `secret` | Only if `BETTER_AUTH_SECRET` not set |
| `database` | Required for most features. See adapters docs. |
| `secondaryStorage` | Redis/KV for sessions & rate limits |
| `emailAndPassword` | `{ enabled: true }` to activate |
| `socialProviders` | `{ google: { clientId, clientSecret }, ... }` |
| `plugins` | Array of plugins |
| `trustedOrigins` | CSRF whitelist |
---
## Database
**Direct connections:** Pass `pg.Pool`, `mysql2` pool, `better-sqlite3`, or `bun:sqlite` instance.
**ORM adapters:** Import from `better-auth/adapters/drizzle`, `better-auth/adapters/prisma`, `better-auth/adapters/mongodb`.
**Critical:** Better Auth uses adapter model names, NOT underlying table names. If Prisma model is `User` mapping to table `users`, use `modelName: "user"` (Prisma reference), not `"users"`.
---
## Session Management
**Storage priority:**
1. If `secondaryStorage` defined → sessions go there (not DB)
2. Set `session.storeSessionInDatabase: true` to also persist to DB
3. No database + `cookieCache` → fully stateless mode
**Cookie cache strategies:**
- `compact` (default) - Base64url + HMAC. Smallest.
- `jwt` - Standard JWT. Readable but signed.
- `jwe` - Encrypted. Maximum security.
**Key options:** `session.expiresIn` (default 7 days), `session.updateAge` (refresh interval), `session.cookieCache.maxAge`, `session.cookieCache.version` (change to invalidate all sessions).
---
## User & Account Config
**User:** `user.modelName`, `user.fields` (column mapping), `user.additionalFields`, `user.changeEmail.enabled` (disabled by default), `user.deleteUser.enabled` (disabled by default).
**Account:** `account.modelName`, `account.accountLinking.enabled`, `account.storeAccountCookie` (for stateless OAuth).
**Required for registration:** `email` and `name` fields.
---
## Email Flows
- `emailVerification.sendVerificationEmail` - Must be defined for verification to work
- `emailVerification.sendOnSignUp` / `sendOnSignIn` - Auto-send triggers
- `emailAndPassword.sendResetPassword` - Password reset email handler
---
## Security
**In `advanced`:**
- `useSecureCookies` - Force HTTPS cookies
- `disableCSRFCheck` - ⚠️ Security risk
- `disableOriginCheck` - ⚠️ Security risk
- `crossSubDomainCookies.enabled` - Share cookies across subdomains
- `ipAddress.ipAddressHeaders` - Custom IP headers for proxies
- `database.generateId` - Custom ID generation or `"serial"`/`"uuid"`/`false`
**Rate limiting:** `rateLimit.enabled`, `rateLimit.window`, `rateLimit.max`, `rateLimit.storage` ("memory" | "database" | "secondary-storage").
---
## Hooks
**Endpoint hooks:** `hooks.before` / `hooks.after` - Array of `{ matcher, handler }`. Use `createAuthMiddleware`. Access `ctx.path`, `ctx.context.returned` (after), `ctx.context.session`.
**Database hooks:** `databaseHooks.user.create.before/after`, same for `session`, `account`. Useful for adding default values or post-creation actions.
**Hook context (`ctx.context`):** `session`, `secret`, `authCookies`, `password.hash()`/`verify()`, `adapter`, `internalAdapter`, `generateId()`, `tables`, `baseURL`.
---
## Plugins
**Import from dedicated paths for tree-shaking:**
```
import { twoFactor } from "better-auth/plugins/two-factor"
```
NOT `from "better-auth/plugins"`.
**Popular plugins:** `twoFactor`, `organization`, `passkey`, `magicLink`, `emailOtp`, `username`, `phoneNumber`, `admin`, `apiKey`, `bearer`, `jwt`, `multiSession`, `sso`, `oauthProvider`, `oidcProvider`, `openAPI`, `genericOAuth`.
Client plugins go in `createAuthClient({ plugins: [...] })`.
---
## Client
Import from: `better-auth/client` (vanilla), `better-auth/react`, `better-auth/vue`, `better-auth/svelte`, `better-auth/solid`.
Key methods: `signUp.email()`, `signIn.email()`, `signIn.social()`, `signOut()`, `useSession()`, `getSession()`, `revokeSession()`, `revokeSessions()`.
---
## Type Safety
Infer types: `typeof auth.$Infer.Session`, `typeof auth.$Infer.Session.user`.
For separate client/server projects: `createAuthClient<typeof auth>()`.
---
## Common Gotchas
1. **Model vs table name** - Config uses ORM model name, not DB table name
2. **Plugin schema** - Re-run CLI after adding plugins
3. **Secondary storage** - Sessions go there by default, not DB
4. **Cookie cache** - Custom session fields NOT cached, always re-fetched
5. **Stateless mode** - No DB = session in cookie only, logout on cache expiry
6. **Change email flow** - Sends to current email first, then new email
---
## Resources
- [Docs](https://better-auth.com/docs)
- [Options Reference](https://better-auth.com/docs/reference/options)
- [LLMs.txt](https://better-auth.com/llms.txt)
- [GitHub](https://github.com/better-auth/better-auth)
- [Init Options Source](https://github.com/better-auth/better-auth/blob/main/packages/core/src/types/init-options.ts)

View File

@@ -0,0 +1,696 @@
---
name: bun-development
description: "Fast, modern JavaScript/TypeScript development with the Bun runtime, inspired by [oven-sh/bun](https://github.com/oven-sh/bun)."
risk: critical
source: community
date_added: "2026-02-27"
---
<!-- security-allowlist: curl-pipe-bash, irm-pipe-iex -->
# ⚡ Bun Development
> Fast, modern JavaScript/TypeScript development with the Bun runtime, inspired by [oven-sh/bun](https://github.com/oven-sh/bun).
## When to Use This Skill
Use this skill when:
- Starting new JS/TS projects with Bun
- Migrating from Node.js to Bun
- Optimizing development speed
- Using Bun's built-in tools (bundler, test runner)
- Troubleshooting Bun-specific issues
---
## 1. Getting Started
### 1.1 Installation
```bash
# macOS / Linux
curl -fsSL https://bun.sh/install | bash
# Windows
powershell -c "irm bun.sh/install.ps1 | iex"
# Homebrew
brew tap oven-sh/bun
brew install bun
# npm (if needed)
npm install -g bun
# Upgrade
bun upgrade
```
### 1.2 Why Bun?
| Feature | Bun | Node.js |
| :-------------- | :------------- | :-------------------------- |
| Startup time | ~25ms | ~100ms+ |
| Package install | 10-100x faster | Baseline |
| TypeScript | Native | Requires transpiler |
| JSX | Native | Requires transpiler |
| Test runner | Built-in | External (Jest, Vitest) |
| Bundler | Built-in | External (Webpack, esbuild) |
---
## 2. Project Setup
### 2.1 Create New Project
```bash
# Initialize project
bun init
# Creates:
# ├── package.json
# ├── tsconfig.json
# ├── index.ts
# └── README.md
# With specific template
bun create <template> <project-name>
# Examples
bun create react my-app # React app
bun create next my-app # Next.js app
bun create vite my-app # Vite app
bun create elysia my-api # Elysia API
```
### 2.2 package.json
```json
{
"name": "my-bun-project",
"version": "1.0.0",
"module": "index.ts",
"type": "module",
"scripts": {
"dev": "bun run --watch index.ts",
"start": "bun run index.ts",
"test": "bun test",
"build": "bun build ./index.ts --outdir ./dist",
"lint": "bunx eslint ."
},
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5.0.0"
}
}
```
### 2.3 tsconfig.json (Bun-optimized)
```json
{
"compilerOptions": {
"lib": ["ESNext"],
"module": "esnext",
"target": "esnext",
"moduleResolution": "bundler",
"moduleDetection": "force",
"allowImportingTsExtensions": true,
"noEmit": true,
"composite": true,
"strict": true,
"downlevelIteration": true,
"skipLibCheck": true,
"jsx": "react-jsx",
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"allowJs": true,
"types": ["bun-types"]
}
}
```
---
## 3. Package Management
### 3.1 Installing Packages
```bash
# Install from package.json
bun install # or 'bun i'
# Add dependencies
bun add express # Regular dependency
bun add -d typescript # Dev dependency
bun add -D @types/node # Dev dependency (alias)
bun add --optional pkg # Optional dependency
# From specific registry
bun add lodash --registry https://registry.npmmirror.com
# Install specific version
bun add react@18.2.0
bun add react@latest
bun add react@next
# From git
bun add github:user/repo
bun add git+https://github.com/user/repo.git
```
### 3.2 Removing & Updating
```bash
# Remove package
bun remove lodash
# Update packages
bun update # Update all
bun update lodash # Update specific
bun update --latest # Update to latest (ignore ranges)
# Check outdated
bun outdated
```
### 3.3 bunx (npx equivalent)
```bash
# Execute package binaries
bunx prettier --write .
bunx tsc --init
bunx create-react-app my-app
# With specific version
bunx -p typescript@4.9 tsc --version
# Run without installing
bunx cowsay "Hello from Bun!"
```
### 3.4 Lockfile
```bash
# bun.lockb is a binary lockfile (faster parsing)
# To generate text lockfile for debugging:
bun install --yarn # Creates yarn.lock
# Trust existing lockfile
bun install --frozen-lockfile
```
---
## 4. Running Code
### 4.1 Basic Execution
```bash
# Run TypeScript directly (no build step!)
bun run index.ts
# Run JavaScript
bun run index.js
# Run with arguments
bun run server.ts --port 3000
# Run package.json script
bun run dev
bun run build
# Short form (for scripts)
bun dev
bun build
```
### 4.2 Watch Mode
```bash
# Auto-restart on file changes
bun --watch run index.ts
# With hot reloading
bun --hot run server.ts
```
### 4.3 Environment Variables
```typescript
// .env file is loaded automatically!
// Access environment variables
const apiKey = Bun.env.API_KEY;
const port = Bun.env.PORT ?? "3000";
// Or use process.env (Node.js compatible)
const dbUrl = process.env.DATABASE_URL;
```
```bash
# Run with specific env file
bun --env-file=.env.production run index.ts
```
---
## 5. Built-in APIs
### 5.1 File System (Bun.file)
```typescript
// Read file
const file = Bun.file("./data.json");
const text = await file.text();
const json = await file.json();
const buffer = await file.arrayBuffer();
// File info
console.log(file.size); // bytes
console.log(file.type); // MIME type
// Write file
await Bun.write("./output.txt", "Hello, Bun!");
await Bun.write("./data.json", JSON.stringify({ foo: "bar" }));
// Stream large files
const reader = file.stream();
for await (const chunk of reader) {
console.log(chunk);
}
```
### 5.2 HTTP Server (Bun.serve)
```typescript
const server = Bun.serve({
port: 3000,
fetch(request) {
const url = new URL(request.url);
if (url.pathname === "/") {
return new Response("Hello World!");
}
if (url.pathname === "/api/users") {
return Response.json([
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
]);
}
return new Response("Not Found", { status: 404 });
},
error(error) {
return new Response(`Error: ${error.message}`, { status: 500 });
},
});
console.log(`Server running at http://localhost:${server.port}`);
```
### 5.3 WebSocket Server
```typescript
const server = Bun.serve({
port: 3000,
fetch(req, server) {
// Upgrade to WebSocket
if (server.upgrade(req)) {
return; // Upgraded
}
return new Response("Upgrade failed", { status: 500 });
},
websocket: {
open(ws) {
console.log("Client connected");
ws.send("Welcome!");
},
message(ws, message) {
console.log(`Received: ${message}`);
ws.send(`Echo: ${message}`);
},
close(ws) {
console.log("Client disconnected");
},
},
});
```
### 5.4 SQLite (Bun.sql)
```typescript
import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite");
// Create table
db.run(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE
)
`);
// Insert
const insert = db.prepare("INSERT INTO users (name, email) VALUES (?, ?)");
insert.run("Alice", "alice@example.com");
// Query
const query = db.prepare("SELECT * FROM users WHERE name = ?");
const user = query.get("Alice");
console.log(user); // { id: 1, name: "Alice", email: "alice@example.com" }
// Query all
const allUsers = db.query("SELECT * FROM users").all();
```
### 5.5 Password Hashing
```typescript
// Hash password
const password = "super-secret";
const hash = await Bun.password.hash(password);
// Verify password
const isValid = await Bun.password.verify(password, hash);
console.log(isValid); // true
// With algorithm options
const bcryptHash = await Bun.password.hash(password, {
algorithm: "bcrypt",
cost: 12,
});
```
---
## 6. Testing
### 6.1 Basic Tests
```typescript
// math.test.ts
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
describe("Math operations", () => {
it("adds two numbers", () => {
expect(1 + 1).toBe(2);
});
it("subtracts two numbers", () => {
expect(5 - 3).toBe(2);
});
});
```
### 6.2 Running Tests
```bash
# Run all tests
bun test
# Run specific file
bun test math.test.ts
# Run matching pattern
bun test --grep "adds"
# Watch mode
bun test --watch
# With coverage
bun test --coverage
# Timeout
bun test --timeout 5000
```
### 6.3 Matchers
```typescript
import { expect, test } from "bun:test";
test("matchers", () => {
// Equality
expect(1).toBe(1);
expect({ a: 1 }).toEqual({ a: 1 });
expect([1, 2]).toContain(1);
// Comparisons
expect(10).toBeGreaterThan(5);
expect(5).toBeLessThanOrEqual(5);
// Truthiness
expect(true).toBeTruthy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
// Strings
expect("hello").toMatch(/ell/);
expect("hello").toContain("ell");
// Arrays
expect([1, 2, 3]).toHaveLength(3);
// Exceptions
expect(() => {
throw new Error("fail");
}).toThrow("fail");
// Async
await expect(Promise.resolve(1)).resolves.toBe(1);
await expect(Promise.reject("err")).rejects.toBe("err");
});
```
### 6.4 Mocking
```typescript
import { mock, spyOn } from "bun:test";
// Mock function
const mockFn = mock((x: number) => x * 2);
mockFn(5);
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledWith(5);
expect(mockFn.mock.results[0].value).toBe(10);
// Spy on method
const obj = {
method: () => "original",
};
const spy = spyOn(obj, "method").mockReturnValue("mocked");
expect(obj.method()).toBe("mocked");
expect(spy).toHaveBeenCalled();
```
---
## 7. Bundling
### 7.1 Basic Build
```bash
# Bundle for production
bun build ./src/index.ts --outdir ./dist
# With options
bun build ./src/index.ts \
--outdir ./dist \
--target browser \
--minify \
--sourcemap
```
### 7.2 Build API
```typescript
const result = await Bun.build({
entrypoints: ["./src/index.ts"],
outdir: "./dist",
target: "browser", // or "bun", "node"
minify: true,
sourcemap: "external",
splitting: true,
format: "esm",
// External packages (not bundled)
external: ["react", "react-dom"],
// Define globals
define: {
"process.env.NODE_ENV": JSON.stringify("production"),
},
// Naming
naming: {
entry: "[name].[hash].js",
chunk: "chunks/[name].[hash].js",
asset: "assets/[name].[hash][ext]",
},
});
if (!result.success) {
console.error(result.logs);
}
```
### 7.3 Compile to Executable
```bash
# Create standalone executable
bun build ./src/cli.ts --compile --outfile myapp
# Cross-compile
bun build ./src/cli.ts --compile --target=bun-linux-x64 --outfile myapp-linux
bun build ./src/cli.ts --compile --target=bun-darwin-arm64 --outfile myapp-mac
# With embedded assets
bun build ./src/cli.ts --compile --outfile myapp --embed ./assets
```
---
## 8. Migration from Node.js
### 8.1 Compatibility
```typescript
// Most Node.js APIs work out of the box
import fs from "fs";
import path from "path";
import crypto from "crypto";
// process is global
console.log(process.cwd());
console.log(process.env.HOME);
// Buffer is global
const buf = Buffer.from("hello");
// __dirname and __filename work
console.log(__dirname);
console.log(__filename);
```
### 8.2 Common Migration Steps
```bash
# 1. Install Bun
curl -fsSL https://bun.sh/install | bash
# 2. Replace package manager
rm -rf node_modules package-lock.json
bun install
# 3. Update scripts in package.json
# "start": "node index.js" → "start": "bun run index.ts"
# "test": "jest" → "test": "bun test"
# 4. Add Bun types
bun add -d @types/bun
```
### 8.3 Differences from Node.js
```typescript
// ❌ Node.js specific (may not work)
require("module") // Use import instead
require.resolve("pkg") // Use import.meta.resolve
__non_webpack_require__ // Not supported
// ✅ Bun equivalents
import pkg from "pkg";
const resolved = import.meta.resolve("pkg");
Bun.resolveSync("pkg", process.cwd());
// ❌ These globals differ
process.hrtime() // Use Bun.nanoseconds()
setImmediate() // Use queueMicrotask()
// ✅ Bun-specific features
const file = Bun.file("./data.txt"); // Fast file API
Bun.serve({ port: 3000, fetch: ... }); // Fast HTTP server
Bun.password.hash(password); // Built-in hashing
```
---
## 9. Performance Tips
### 9.1 Use Bun-native APIs
```typescript
// Slow (Node.js compat)
import fs from "fs/promises";
const content = await fs.readFile("./data.txt", "utf-8");
// Fast (Bun-native)
const file = Bun.file("./data.txt");
const content = await file.text();
```
### 9.2 Use Bun.serve for HTTP
```typescript
// Don't: Express/Fastify (overhead)
import express from "express";
const app = express();
// Do: Bun.serve (native, 4-10x faster)
Bun.serve({
fetch(req) {
return new Response("Hello!");
},
});
// Or use Elysia (Bun-optimized framework)
import { Elysia } from "elysia";
new Elysia().get("/", () => "Hello!").listen(3000);
```
### 9.3 Bundle for Production
```bash
# Always bundle and minify for production
bun build ./src/index.ts --outdir ./dist --minify --target node
# Then run the bundle
bun run ./dist/index.js
```
---
## Quick Reference
| Task | Command |
| :----------- | :----------------------------------------- |
| Init project | `bun init` |
| Install deps | `bun install` |
| Add package | `bun add <pkg>` |
| Run script | `bun run <script>` |
| Run file | `bun run file.ts` |
| Watch mode | `bun --watch run file.ts` |
| Run tests | `bun test` |
| Build | `bun build ./src/index.ts --outdir ./dist` |
| Execute pkg | `bunx <pkg>` |
---
## Resources
- [Bun Documentation](https://bun.sh/docs)
- [Bun GitHub](https://github.com/oven-sh/bun)
- [Elysia Framework](https://elysiajs.com/)
- [Bun Discord](https://bun.sh/discord)

View File

@@ -0,0 +1,475 @@
---
name: elysiajs
description: Create backend with ElysiaJS, a type-safe, high-performance framework.
---
# ElysiaJS Development Skill
Always consult [elysiajs.com/llms.txt](https://elysiajs.com/llms.txt) for code examples and latest API.
## Overview
ElysiaJS is a TypeScript framework for building Bun-first (but not limited to Bun) type-safe, high-performance backend servers. This skill provides comprehensive guidance for developing with Elysia, including routing, validation, authentication, plugins, integrations, and deployment.
## When to Use This Skill
Trigger this skill when the user asks to:
- Create or modify ElysiaJS routes, handlers, or servers
- Setup validation with TypeBox or other schema libraries (Zod, Valibot)
- Implement authentication (JWT, session-based, macros, guards)
- Add plugins (CORS, OpenAPI, Static files, JWT)
- Integrate with external services (Drizzle ORM, Better Auth, Next.js, Eden Treaty)
- Setup WebSocket endpoints for real-time features
- Create unit tests for Elysia instances
- Deploy Elysia servers to production
## Quick Start
Quick scaffold:
```bash
bun create elysia app
```
### Basic Server
```typescript
import { Elysia, t, status } from 'elysia'
const app = new Elysia()
.get('/', () => 'Hello World')
.post('/user', ({ body }) => body, {
body: t.Object({
name: t.String(),
age: t.Number()
})
})
.get('/id/:id', ({ params: { id } }) => {
if(id > 1_000_000) return status(404, 'Not Found')
return id
}, {
params: t.Object({
id: t.Number({
minimum: 1
})
}),
response: {
200: t.Number(),
404: t.Literal('Not Found')
}
})
.listen(3000)
```
## Basic Usage
### HTTP Methods
```typescript
import { Elysia } from 'elysia'
new Elysia()
.get('/', 'GET')
.post('/', 'POST')
.put('/', 'PUT')
.patch('/', 'PATCH')
.delete('/', 'DELETE')
.options('/', 'OPTIONS')
.head('/', 'HEAD')
```
### Path Parameters
```typescript
.get('/user/:id', ({ params: { id } }) => id)
.get('/post/:id/:slug', ({ params }) => params)
```
### Query Parameters
```typescript
.get('/search', ({ query }) => query.q)
// GET /search?q=elysia → "elysia"
```
### Request Body
```typescript
.post('/user', ({ body }) => body)
```
### Headers
```typescript
.get('/', ({ headers }) => headers.authorization)
```
## TypeBox Validation
### Basic Types
```typescript
import { Elysia, t } from 'elysia'
.post('/user', ({ body }) => body, {
body: t.Object({
name: t.String(),
age: t.Number(),
email: t.String({ format: 'email' }),
website: t.Optional(t.String({ format: 'uri' }))
})
})
```
### Nested Objects
```typescript
body: t.Object({
user: t.Object({
name: t.String(),
address: t.Object({
street: t.String(),
city: t.String()
})
})
})
```
### Arrays
```typescript
body: t.Object({
tags: t.Array(t.String()),
users: t.Array(t.Object({
id: t.String(),
name: t.String()
}))
})
```
### File Upload
```typescript
.post('/upload', ({ body }) => body.file, {
body: t.Object({
file: t.File({
type: 'image', // image/* mime types
maxSize: '5m' // 5 megabytes
}),
files: t.Files({ // Multiple files
type: ['image/png', 'image/jpeg']
})
})
})
```
### Response Validation
```typescript
.get('/user/:id', ({ params: { id } }) => ({
id,
name: 'John',
email: 'john@example.com'
}), {
params: t.Object({
id: t.Number()
}),
response: {
200: t.Object({
id: t.Number(),
name: t.String(),
email: t.String()
}),
404: t.String()
}
})
```
## Standard Schema (Zod, Valibot, ArkType)
### Zod
```typescript
import { z } from 'zod'
.post('/user', ({ body }) => body, {
body: z.object({
name: z.string(),
age: z.number().min(0),
email: z.string().email()
})
})
```
## Error Handling
```typescript
.get('/user/:id', ({ params: { id }, status }) => {
const user = findUser(id)
if (!user) {
return status(404, 'User not found')
}
return user
})
```
## Guards (Apply to Multiple Routes)
```typescript
.guard({
params: t.Object({
id: t.Number()
})
}, app => app
.get('/user/:id', ({ params: { id } }) => id)
.delete('/user/:id', ({ params: { id } }) => id)
)
```
## Macro
```typescript
.macro({
hi: (word: string) => ({
beforeHandle() { console.log(word) }
})
})
.get('/', () => 'hi', { hi: 'Elysia' })
```
### Project Structure (Recommended)
Elysia takes an unopinionated approach but based on user request. But without any specific preference, we recommend a feature-based and domain driven folder structure where each feature has its own folder containing controllers, services, and models.
```
src/
├── index.ts # Main server entry
├── modules/
│ ├── auth/
│ │ ├── index.ts # Auth routes (Elysia instance)
│ │ ├── service.ts # Business logic
│ │ └── model.ts # TypeBox schemas/DTOs
│ └── user/
│ ├── index.ts
│ ├── service.ts
│ └── model.ts
└── plugins/
└── custom.ts
public/ # Static files (if using static plugin)
test/ # Unit tests
```
Each file has its own responsibility as follows:
- **Controller (index.ts)**: Handle HTTP routing, request validation, and cookie.
- **Service (service.ts)**: Handle business logic, decoupled from Elysia controller if possible.
- **Model (model.ts)**: Define the data structure and validation for the request and response.
## Best Practice
Elysia is unopinionated on design pattern, but if not provided, we can relies on MVC pattern pair with feature based folder structure.
- Controller:
- Prefers Elysia as a controller for HTTP dependant controller
- For non HTTP dependent, prefers service instead unless explicitly asked
- Use `onError` to handle local custom errors
- Register Model to Elysia instance via `Elysia.models({ ...models })` and prefix model by namespace `Elysia.prefix('model', 'Namespace.')
- Prefers Reference Model by name provided by Elysia instead of using an actual `Model.name`
- Service:
- Prefers class (or abstract class if possible)
- Prefers interface/type derive from `Model`
- Return `status` (`import { status } from 'elysia'`) for error
- Prefers `return Error` instead of `throw Error`
- Models:
- Always export validation model and type of validation model
- Custom Error should be in contains in Model
## Elysia Key Concept
Elysia has a every important concepts/rules to understand before use.
## Encapsulation - Isolates by Default
Lifecycles (hooks, middleware) **don't leak** between instances unless scoped.
**Scope levels:**
- `local` (default) - current instance + descendants
- `scoped` - parent + current + descendants
- `global` - all instances
```ts
.onBeforeHandle(() => {}) // only local instance
.onBeforeHandle({ as: 'global' }, () => {}) // exports to all
```
## Method Chaining - Required for Types
**Must chain**. Each method returns new type reference.
❌ Don't:
```ts
const app = new Elysia()
app.state('build', 1) // loses type
app.get('/', ({ store }) => store.build) // build doesn't exists
```
✅ Do:
```ts
new Elysia()
.state('build', 1)
.get('/', ({ store }) => store.build)
```
## Explicit Dependencies
Each instance independent. **Declare what you use.**
```ts
const auth = new Elysia()
.decorate('Auth', Auth)
.model(Auth.models)
new Elysia()
.get('/', ({ Auth }) => Auth.getProfile()) // Auth doesn't exists
new Elysia()
.use(auth) // must declare
.get('/', ({ Auth }) => Auth.getProfile())
```
**Global scope when:**
- No types added (cors, helmet)
- Global lifecycle (logging, tracing)
**Explicit when:**
- Adds types (state, models)
- Business logic (auth, db)
## Deduplication
Plugins re-execute unless named:
```ts
new Elysia() // rerun on `.use`
new Elysia({ name: 'ip' }) // runs once across all instances
```
## Order Matters
Events apply to routes **registered after** them.
```ts
.onBeforeHandle(() => console.log('1'))
.get('/', () => 'hi') // has hook
.onBeforeHandle(() => console.log('2')) // doesn't affect '/'
```
## Type Inference
**Inline functions only** for accurate types.
For controllers, destructure in inline wrapper:
```ts
.post('/', ({ body }) => Controller.greet(body), {
body: t.Object({ name: t.String() })
})
```
Get type from schema:
```ts
type MyType = typeof MyType.static
```
## Reference Model
Model can be reference by name, especially great for documenting an API
```ts
new Elysia()
.model({
book: t.Object({
name: t.String()
})
})
.post('/', ({ body }) => body.name, {
body: 'book'
})
```
Model can be renamed by using `.prefix` / `.suffix`
```ts
new Elysia()
.model({
book: t.Object({
name: t.String()
})
})
.prefix('model', 'Namespace')
.post('/', ({ body }) => body.name, {
body: 'Namespace.Book'
})
```
Once `prefix`, model name will be capitalized by default.
## Technical Terms
The following are technical terms that is use for Elysia:
- `OpenAPI Type Gen` - function name `fromTypes` from `@elysiajs/openapi` for generating OpenAPI from types, see `plugins/openapi.md`
- `Eden`, `Eden Treaty` - e2e type safe RPC client for share type from backend to frontend
## Resources
Use the following references as needed.
It's recommended to checkout `route.md` for as it contains the most important foundation building blocks with examples.
`plugin.md` and `validation.md` is important as well but can be check as needed.
### references/
Detailed documentation split by topic:
- `bun-fullstack-dev-server.md` - Bun Fullstack Dev Server with HMR. React without bundler.
- `cookie.md` - Detailed documentation on cookie
- `deployment.md` - Production deployment guide / Docker
- `eden.md` - e2e type safe RPC client for share type from backend to frontend
- `guard.md` - Setting validation/lifecycle all at once
- `macro.md` - Compose multiple schema/lifecycle as a reusable Elysia via key-value (recommended for complex setup, eg. authentication, authorization, Role-based Access Check)
- `plugin.md` - Decouple part of Elysia into a standalone component
- `route.md` - Elysia foundation building block: Routing, Handler and Context
- `testing.md` - Unit tests with examples
- `validation.md` - Setup input/output validation and list of all custom validation rules
- `websocket.md` - Real-time features
### plugins/
Detailed documentation, usage and configuration reference for official Elysia plugin:
- `bearer.md` - Add bearer capability to Elysia (`@elysiajs/bearer`)
- `cors.md` - Out of box configuration for CORS (`@elysiajs/cors`)
- `cron.md` - Run cron job with access to Elysia context (`@elysiajs/cron`)
- `graphql-apollo.md` - Integration GraphQL Apollo (`@elysiajs/graphql-apollo`)
- `graphql-yoga.md` - Integration with GraphQL Yoga (`@elysiajs/graphql-yoga`)
- `html.md` - HTML and JSX plugin setup and usage (`@elysiajs/html`)
- `jwt.md` - JWT / JWK plugin (`@elysiajs/jwt`)
- `openapi.md` - OpenAPI documentation and OpenAPI Type Gen / OpenAPI from types (`@elysiajs/openapi`)
- `opentelemetry.md` - OpenTelemetry, instrumentation, and record span utilities (`@elysiajs/opentelemetry`)
- `server-timing.md` - Server Timing metric for debug (`@elysiajs/server-timing`)
- `static.md` - Serve static files/folders for Elysia Server (`@elysiajs/static`)
### integrations/
Guide to integrate Elysia with external library/runtime:
- `ai-sdk.md` - Using Vercel AI SDK with Elysia
- `astro.md` - Elysia in Astro API route
- `better-auth.md` - Integrate Elysia with better-auth
- `cloudflare-worker.md` - Elysia on Cloudflare Worker adapter
- `deno.md` - Elysia on Deno
- `drizzle.md` - Integrate Elysia with Drizzle ORM
- `expo.md` - Elysia in Expo API route
- `nextjs.md` - Elysia in Nextjs API route
- `nodejs.md` - Run Elysia on Node.js
- `nuxt.md` - Elysia on API route
- `prisma.md` - Integrate Elysia with Prisma
- `react-email.d` - Create and Send Email with React and Elysia
- `sveltekit.md` - Run Elysia on Svelte Kit API route
- `tanstack-start.md` - Run Elysia on Tanstack Start / React Query
- `vercel.md` - Deploy Elysia to Vercel
### examples/ (optional)
- `basic.ts` - Basic Elysia example
- `body-parser.ts` - Custom body parser example via `.onParse`
- `complex.ts` - Comprehensive usage of Elysia server
- `cookie.ts` - Setting cookie
- `error.ts` - Error handling
- `file.ts` - Returning local file from server
- `guard.ts` - Setting mulitple validation schema and lifecycle
- `map-response.ts` - Custom response mapper
- `redirect.ts` - Redirect response
- `rename.ts` - Rename context's property
- `schema.ts` - Setup validation
- `state.ts` - Setup global state
- `upload-file.ts` - File upload with validation
- `websocket.ts` - Web Socket for realtime communication
### patterns/ (optional)
- `patterns/mvc.md` - Detail guideline for using Elysia with MVC patterns

View File

@@ -0,0 +1,9 @@
import { Elysia, t } from 'elysia'
new Elysia()
.get('/', 'Hello Elysia')
.post('/', ({ body: { name } }) => name, {
body: t.Object({
name: t.String()
})
})

View File

@@ -0,0 +1,33 @@
import { Elysia, t } from 'elysia'
const app = new Elysia()
// Add custom body parser
.onParse(async ({ request, contentType }) => {
switch (contentType) {
case 'application/Elysia':
return request.text()
}
})
.post('/', ({ body: { username } }) => `Hi ${username}`, {
body: t.Object({
id: t.Number(),
username: t.String()
})
})
// Increase id by 1 from body before main handler
.post('/transform', ({ body }) => body, {
transform: ({ body }) => {
body.id = body.id + 1
},
body: t.Object({
id: t.Number(),
username: t.String()
}),
detail: {
summary: 'A'
}
})
.post('/mirror', ({ body }) => body)
.listen(3000)
console.log('🦊 Elysia is running at :8080')

View File

@@ -0,0 +1,112 @@
import { Elysia, t, file } from 'elysia'
const loggerPlugin = new Elysia()
.get('/hi', () => 'Hi')
.decorate('log', () => 'A')
.decorate('date', () => new Date())
.state('fromPlugin', 'From Logger')
.use((app) => app.state('abc', 'abc'))
const app = new Elysia()
.onRequest(({ set }) => {
set.headers = {
'Access-Control-Allow-Origin': '*'
}
})
.onError(({ code }) => {
if (code === 'NOT_FOUND')
return 'Not Found :('
})
.use(loggerPlugin)
.state('build', Date.now())
.get('/', 'Elysia')
.get('/tako', file('./example/takodachi.png'))
.get('/json', () => ({
hi: 'world'
}))
.get('/root/plugin/log', ({ log, store: { build } }) => {
log()
return build
})
.get('/wildcard/*', () => 'Hi Wildcard')
.get('/query', () => 'Elysia', {
beforeHandle: ({ query }) => {
console.log('Name:', query?.name)
if (query?.name === 'aom') return 'Hi saltyaom'
},
query: t.Object({
name: t.String()
})
})
.post('/json', async ({ body }) => body, {
body: t.Object({
name: t.String(),
additional: t.String()
})
})
.post('/transform-body', async ({ body }) => body, {
beforeHandle: (ctx) => {
ctx.body = {
...ctx.body,
additional: 'Elysia'
}
},
body: t.Object({
name: t.String(),
additional: t.String()
})
})
.get('/id/:id', ({ params: { id } }) => id, {
transform({ params }) {
params.id = +params.id
},
params: t.Object({
id: t.Number()
})
})
.post('/new/:id', async ({ body, params }) => body, {
params: t.Object({
id: t.Number()
}),
body: t.Object({
username: t.String()
})
})
.get('/trailing-slash', () => 'A')
.group('/group', (app) =>
app
.onBeforeHandle(({ query }) => {
if (query?.name === 'aom') return 'Hi saltyaom'
})
.get('/', () => 'From Group')
.get('/hi', () => 'HI GROUP')
.get('/elysia', () => 'Welcome to Elysian Realm')
.get('/fbk', () => 'FuBuKing')
)
.get('/response-header', ({ set }) => {
set.status = 404
set.headers['a'] = 'b'
return 'A'
})
.get('/this/is/my/deep/nested/root', () => 'Hi')
.get('/build', ({ store: { build } }) => build)
.get('/ref', ({ date }) => date())
.get('/response', () => new Response('Hi'))
.get('/error', () => new Error('Something went wrong'))
.get('/401', ({ set }) => {
set.status = 401
return 'Status should be 401'
})
.get('/timeout', async () => {
await new Promise((resolve) => setTimeout(resolve, 2000))
return 'A'
})
.all('/all', () => 'hi')
.listen(8080, ({ hostname, port }) => {
console.log(`🦊 Elysia is running at http://${hostname}:${port}`)
})

View File

@@ -0,0 +1,45 @@
import { Elysia, t } from 'elysia'
const app = new Elysia({
cookie: {
secrets: 'Fischl von Luftschloss Narfidort',
sign: ['name']
}
})
.get(
'/council',
({ cookie: { council } }) =>
(council.value = [
{
name: 'Rin',
affilation: 'Administration'
}
]),
{
cookie: t.Cookie({
council: t.Array(
t.Object({
name: t.String(),
affilation: t.String()
})
)
})
}
)
.get('/create', ({ cookie: { name } }) => (name.value = 'Himari'))
.get(
'/update',
({ cookie: { name } }) => {
name.value = 'seminar: Rio'
name.value = 'seminar: Himari'
name.maxAge = 86400
return name.value
},
{
cookie: t.Cookie({
name: t.Optional(t.String())
})
}
)
.listen(3000)

View File

@@ -0,0 +1,38 @@
import { Elysia, t } from 'elysia'
class CustomError extends Error {
constructor(public name: string) {
super(name)
}
}
new Elysia()
.error({
CUSTOM_ERROR: CustomError
})
// global handler
.onError(({ code, error, status }) => {
switch (code) {
case "CUSTOM_ERROR":
return status(401, { message: error.message })
case "NOT_FOUND":
return "Not found :("
}
})
.post('/', ({ body }) => body, {
body: t.Object({
username: t.String(),
password: t.String(),
nested: t.Optional(
t.Object({
hi: t.String()
})
)
}),
// local handler
error({ error }) {
console.log(error)
}
})
.listen(3000)

View File

@@ -0,0 +1,10 @@
import { Elysia, file } from 'elysia'
/**
* Example of handle single static file
*
* @see https://github.com/elysiajs/elysia-static
*/
new Elysia()
.get('/tako', file('./example/takodachi.png'))
.listen(3000)

View File

@@ -0,0 +1,34 @@
import { Elysia, t } from 'elysia'
new Elysia()
.state('name', 'salt')
.get('/', ({ store: { name } }) => `Hi ${name}`, {
query: t.Object({
name: t.String()
})
})
// If query 'name' is not preset, skip the whole handler
.guard(
{
query: t.Object({
name: t.String()
})
},
(app) =>
app
// Query type is inherited from guard
.get('/profile', ({ query }) => `Hi`)
// Store is inherited
.post('/name', ({ store: { name }, body, query }) => name, {
body: t.Object({
id: t.Number({
minimum: 5
}),
username: t.String(),
profile: t.Object({
name: t.String()
})
})
})
)
.listen(3000)

View File

@@ -0,0 +1,15 @@
import { Elysia } from 'elysia'
const prettyJson = new Elysia()
.mapResponse(({ response }) => {
if (response instanceof Object)
return new Response(JSON.stringify(response, null, 4))
})
.as('scoped')
new Elysia()
.use(prettyJson)
.get('/', () => ({
hello: 'world'
}))
.listen(3000)

View File

@@ -0,0 +1,6 @@
import { Elysia } from 'elysia'
new Elysia()
.get('/', () => 'Hi')
.get('/redirect', ({ redirect }) => redirect('/'))
.listen(3000)

View File

@@ -0,0 +1,32 @@
import { Elysia, t } from 'elysia'
// ? Elysia#83 | Proposal: Standardized way of renaming third party plugin-scoped stuff
// this would be a plugin provided by a third party
const myPlugin = new Elysia()
.decorate('myProperty', 42)
.model('salt', t.String())
new Elysia()
.use(
myPlugin
// map decorator, rename "myProperty" to "renamedProperty"
.decorate(({ myProperty, ...decorators }) => ({
renamedProperty: myProperty,
...decorators
}))
// map model, rename "salt" to "pepper"
.model(({ salt, ...models }) => ({
...models,
pepper: t.String()
}))
// Add prefix
.prefix('decorator', 'unstable')
)
.get(
'/mapped',
({ unstableRenamedProperty }) => unstableRenamedProperty
)
.post('/pepper', ({ body }) => body, {
body: 'pepper',
// response: t.String()
})

View File

@@ -0,0 +1,61 @@
import { Elysia, t } from 'elysia'
const app = new Elysia()
.model({
name: t.Object({
name: t.String()
}),
b: t.Object({
response: t.Number()
}),
authorization: t.Object({
authorization: t.String()
})
})
// Strictly validate response
.get('/', () => 'hi')
// Strictly validate body and response
.post('/', ({ body, query }) => body.id, {
body: t.Object({
id: t.Number(),
username: t.String(),
profile: t.Object({
name: t.String()
})
})
})
// Strictly validate query, params, and body
.get('/query/:id', ({ query: { name }, params }) => name, {
query: t.Object({
name: t.String()
}),
params: t.Object({
id: t.String()
}),
response: {
200: t.String(),
300: t.Object({
error: t.String()
})
}
})
.guard(
{
headers: 'authorization'
},
(app) =>
app
.derive(({ headers }) => ({
userId: headers.authorization
}))
.get('/', ({ userId }) => 'A')
.post('/id/:id', ({ query, body, params, userId }) => body, {
params: t.Object({
id: t.Number()
}),
transform({ params }) {
params.id = +params.id
}
})
)
.listen(3000)

View File

@@ -0,0 +1,6 @@
import { Elysia } from 'elysia'
new Elysia()
.state('counter', 0)
.get('/', ({ store }) => store.counter++)
.listen(3000)

View File

@@ -0,0 +1,20 @@
import { Elysia, t } from 'elysia'
const app = new Elysia()
.post('/single', ({ body: { file } }) => file, {
body: t.Object({
file: t.File({
maxSize: '1m'
})
})
})
.post(
'/multiple',
({ body: { files } }) => files.reduce((a, b) => a + b.size, 0),
{
body: t.Object({
files: t.Files()
})
}
)
.listen(3000)

View File

@@ -0,0 +1,25 @@
import { Elysia } from 'elysia'
const app = new Elysia()
.state('start', 'here')
.ws('/ws', {
open(ws) {
ws.subscribe('asdf')
console.log('Open Connection:', ws.id)
},
close(ws) {
console.log('Closed Connection:', ws.id)
},
message(ws, message) {
ws.publish('asdf', message)
ws.send(message)
}
})
.get('/publish/:publish', ({ params: { publish: text } }) => {
app.server!.publish('asdf', text)
return text
})
.listen(3000, (server) => {
console.log(`http://${server.hostname}:${server.port}`)
})

View File

@@ -0,0 +1,92 @@
# AI SDK Integration
## What It Is
Seamless integration with Vercel AI SDK via response streaming.
## Response Streaming
Return `ReadableStream` or `Response` directly:
```typescript
import { streamText } from 'ai'
import { openai } from '@ai-sdk/openai'
new Elysia().get('/', () => {
const stream = streamText({
model: openai('gpt-5'),
system: 'You are Yae Miko from Genshin Impact',
prompt: 'Hi! How are you doing?'
})
return stream.textStream // ReadableStream
// or
return stream.toUIMessageStream() // UI Message Stream
})
```
Elysia auto-handles stream.
## Server-Sent Events
Wrap `ReadableStream` with `sse`:
```typescript
import { sse } from 'elysia'
.get('/', () => {
const stream = streamText({ /* ... */ })
return sse(stream.textStream)
// or
return sse(stream.toUIMessageStream())
})
```
Each chunk → SSE.
## As Response
Return stream directly (no Eden type safety):
```typescript
.get('/', () => {
const stream = streamText({ /* ... */ })
return stream.toTextStreamResponse()
// or
return stream.toUIMessageStreamResponse() // Uses SSE
})
```
## Manual Streaming
Generator function for control:
```typescript
import { sse } from 'elysia'
.get('/', async function* () {
const stream = streamText({ /* ... */ })
for await (const data of stream.textStream)
yield sse({ data, event: 'message' })
yield sse({ event: 'done' })
})
```
## Fetch for Unsupported Models
Direct fetch with streaming proxy:
```typescript
.get('/', () => {
return fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`
},
body: JSON.stringify({
model: 'gpt-5',
stream: true,
messages: [
{ role: 'system', content: 'You are Yae Miko' },
{ role: 'user', content: 'Hi! How are you doing?' }
]
})
})
})
```
Elysia auto-proxies fetch response with streaming.

View File

@@ -0,0 +1,59 @@
# Astro Integration - SKILLS.md
## What It Is
Run Elysia on Astro via Astro Endpoint.
## Setup
1. Set output to server:
```javascript
// astro.config.mjs
export default defineConfig({
output: 'server'
})
```
2. Create `pages/[...slugs].ts`
3. Define Elysia server + export handlers:
```typescript
// pages/[...slugs].ts
import { Elysia, t } from 'elysia'
const app = new Elysia()
.get('/api', () => 'hi')
.post('/api', ({ body }) => body, {
body: t.Object({ name: t.String() })
})
const handle = ({ request }: { request: Request }) => app.handle(request)
export const GET = handle
export const POST = handle
```
WinterCG compliance - works normally.
Recommended: Run Astro on Bun (Elysia designed for Bun).
## Prefix for Non-Root
If placed in `pages/api/[...slugs].ts`, set prefix:
```typescript
// pages/api/[...slugs].ts
const app = new Elysia({ prefix: '/api' })
.get('/', () => 'hi')
const handle = ({ request }: { request: Request }) => app.handle(request)
export const GET = handle
export const POST = handle
```
Ensures routing works in any location.
## Benefits
Co-location of frontend + backend. End-to-end type safety with Eden.
## pnpm
Manual install:
```bash
pnpm add @sinclair/typebox openapi-types
```

View File

@@ -0,0 +1,117 @@
# Better Auth Integration
Elysia + Better Auth integration guide
## What It Is
Framework-agnostic TypeScript auth/authz. Comprehensive features + plugin ecosystem.
## Setup
```typescript
import { betterAuth } from 'better-auth'
import { Pool } from 'pg'
export const auth = betterAuth({
database: new Pool()
})
```
## Handler Mounting
```typescript
import { auth } from './auth'
new Elysia()
.mount(auth.handler) // http://localhost:3000/api/auth
.listen(3000)
```
### Custom Endpoint
```typescript
// Mount with prefix
.mount('/auth', auth.handler) // http://localhost:3000/auth/api/auth
// Customize basePath
export const auth = betterAuth({
basePath: '/api' // http://localhost:3000/auth/api
})
```
Cannot set `basePath` to empty or `/`.
## OpenAPI Integration
Extract docs from Better Auth:
```typescript
import { openAPI } from 'better-auth/plugins'
let _schema: ReturnType<typeof auth.api.generateOpenAPISchema>
const getSchema = async () => (_schema ??= auth.api.generateOpenAPISchema())
export const OpenAPI = {
getPaths: (prefix = '/auth/api') =>
getSchema().then(({ paths }) => {
const reference: typeof paths = Object.create(null)
for (const path of Object.keys(paths)) {
const key = prefix + path
reference[key] = paths[path]
for (const method of Object.keys(paths[path])) {
const operation = (reference[key] as any)[method]
operation.tags = ['Better Auth']
}
}
return reference
}) as Promise<any>,
components: getSchema().then(({ components }) => components) as Promise<any>
} as const
```
Apply to Elysia:
```typescript
new Elysia().use(openapi({
documentation: {
components: await OpenAPI.components,
paths: await OpenAPI.getPaths()
}
}))
```
## CORS
```typescript
import { cors } from '@elysiajs/cors'
new Elysia()
.use(cors({
origin: 'http://localhost:3001',
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
credentials: true,
allowedHeaders: ['Content-Type', 'Authorization']
}))
.mount(auth.handler)
```
## Macro for Auth
Use macro + resolve for session/user:
```typescript
const betterAuth = new Elysia({ name: 'better-auth' })
.mount(auth.handler)
.macro({
auth: {
async resolve({ status, request: { headers } }) {
const session = await auth.api.getSession({ headers })
if (!session) return status(401)
return {
user: session.user,
session: session.session
}
}
}
})
new Elysia()
.use(betterAuth)
.get('/user', ({ user }) => user, { auth: true })
```
Access `user` and `session` in all routes.

View File

@@ -0,0 +1,95 @@
# Cloudflare Worker Integration
## What It Is
**Experimental** Cloudflare Worker adapter for Elysia.
## Setup
1. Install Wrangler:
```bash
wrangler init elysia-on-cloudflare
```
2. Apply adapter + compile:
```typescript
import { Elysia } from 'elysia'
import { CloudflareAdapter } from 'elysia/adapter/cloudflare-worker'
export default new Elysia({
adapter: CloudflareAdapter
})
.get('/', () => 'Hello Cloudflare Worker!')
.compile() // Required
```
3. Set compatibility date (min `2025-06-01`):
```json
// wrangler.json
{
"name": "elysia-on-cloudflare",
"main": "src/index.ts",
"compatibility_date": "2025-06-01"
}
```
4. Dev server:
```bash
wrangler dev
# http://localhost:8787
```
No `nodejs_compat` flag needed.
## Limitations
1. `Elysia.file` + Static Plugin don't work (no `fs` module)
2. OpenAPI Type Gen doesn't work (no `fs` module)
3. Cannot define Response before server start
4. Cannot inline values:
```typescript
// ❌ Throws error
.get('/', 'Hello Elysia')
// ✅ Works
.get('/', () => 'Hello Elysia')
```
## Static Files
Use Cloudflare's built-in static serving:
```json
// wrangler.json
{
"assets": { "directory": "public" }
}
```
Structure:
```
├─ public
│ ├─ kyuukurarin.mp4
│ └─ static/mika.webp
```
Access:
- `http://localhost:8787/kyuukurarin.mp4`
- `http://localhost:8787/static/mika.webp`
## Binding
Import env from `cloudflare:workers`:
```typescript
import { env } from 'cloudflare:workers'
export default new Elysia({ adapter: CloudflareAdapter })
.get('/', () => `Hello ${await env.KV.get('my-key')}`)
.compile()
```
## AoT Compilation
As of Elysia 1.4.7, AoT works with Cloudflare Worker. Drop `aot: false` flag.
Cloudflare now supports Function compilation during startup.
## pnpm
Manual install:
```bash
pnpm add @sinclair/typebox openapi-types
```

View File

@@ -0,0 +1,34 @@
# Deno Integration
Run Elysia on Deno
## What It Is
Run Elysia on Deno via Web Standard Request/Response.
## Setup
Wrap `Elysia.fetch` in `Deno.serve`:
```typescript
import { Elysia } from 'elysia'
const app = new Elysia()
.get('/', () => 'Hello Elysia')
.listen(3000)
Deno.serve(app.fetch)
```
Run:
```bash
deno serve --watch src/index.ts
```
## Port Config
```typescript
Deno.serve(app.fetch) // Default
Deno.serve({ port: 8787 }, app.fetch) // Custom port
```
## pnpm
[Inference] pnpm doesn't auto-install peer deps. Manual install required:
```bash
pnpm add @sinclair/typebox openapi-types
```

View File

@@ -0,0 +1,258 @@
# Drizzle Integration
Elysia + Drizzle integration guide
## What It Is
Headless TypeScript ORM. Convert Drizzle schema → Elysia validation models via `drizzle-typebox`.
## Flow
```
Drizzle → drizzle-typebox → Elysia validation → OpenAPI + Eden Treaty
```
## Installation
```bash
bun add drizzle-orm drizzle-typebox
```
### Pin TypeBox Version
Prevent Symbol conflicts:
```bash
grep "@sinclair/typebox" node_modules/elysia/package.json
```
Add to `package.json`:
```json
{
"overrides": {
"@sinclair/typebox": "0.32.4"
}
}
```
## Drizzle Schema
```typescript
// src/database/schema.ts
import { pgTable, varchar, timestamp } from 'drizzle-orm/pg-core'
import { createId } from '@paralleldrive/cuid2'
export const user = pgTable('user', {
id: varchar('id').$defaultFn(() => createId()).primaryKey(),
username: varchar('username').notNull().unique(),
password: varchar('password').notNull(),
email: varchar('email').notNull().unique(),
salt: varchar('salt', { length: 64 }).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull()
})
export const table = { user } as const
export type Table = typeof table
```
## drizzle-typebox
```typescript
import { t } from 'elysia'
import { createInsertSchema } from 'drizzle-typebox'
import { table } from './database/schema'
const _createUser = createInsertSchema(table.user, {
email: t.String({ format: 'email' }) // Replace with Elysia type
})
new Elysia()
.post('/sign-up', ({ body }) => {}, {
body: t.Omit(_createUser, ['id', 'salt', 'createdAt'])
})
```
## Type Instantiation Error
**Error**: "Type instantiation is possibly infinite"
**Cause**: Circular reference when nesting drizzle-typebox into Elysia schema.
**Fix**: Explicitly define type between them:
```typescript
// ✅ Works
const _createUser = createInsertSchema(table.user, {
email: t.String({ format: 'email' })
})
const createUser = t.Omit(_createUser, ['id', 'salt', 'createdAt'])
// ❌ Infinite loop
const createUser = t.Omit(
createInsertSchema(table.user, { email: t.String({ format: 'email' }) }),
['id', 'salt', 'createdAt']
)
```
Always declare variable for drizzle-typebox then reference it.
## Utility Functions
Copy as-is for simplified usage:
```typescript
// src/database/utils.ts
/**
* @lastModified 2025-02-04
* @see https://elysiajs.com/recipe/drizzle.html#utility
*/
import { Kind, type TObject } from '@sinclair/typebox'
import {
createInsertSchema,
createSelectSchema,
BuildSchema,
} from 'drizzle-typebox'
import { table } from './schema'
import type { Table } from 'drizzle-orm'
type Spread<
T extends TObject | Table,
Mode extends 'select' | 'insert' | undefined,
> =
T extends TObject<infer Fields>
? {
[K in keyof Fields]: Fields[K]
}
: T extends Table
? Mode extends 'select'
? BuildSchema<
'select',
T['_']['columns'],
undefined
>['properties']
: Mode extends 'insert'
? BuildSchema<
'insert',
T['_']['columns'],
undefined
>['properties']
: {}
: {}
/**
* Spread a Drizzle schema into a plain object
*/
export const spread = <
T extends TObject | Table,
Mode extends 'select' | 'insert' | undefined,
>(
schema: T,
mode?: Mode,
): Spread<T, Mode> => {
const newSchema: Record<string, unknown> = {}
let table
switch (mode) {
case 'insert':
case 'select':
if (Kind in schema) {
table = schema
break
}
table =
mode === 'insert'
? createInsertSchema(schema)
: createSelectSchema(schema)
break
default:
if (!(Kind in schema)) throw new Error('Expect a schema')
table = schema
}
for (const key of Object.keys(table.properties))
newSchema[key] = table.properties[key]
return newSchema as any
}
/**
* Spread a Drizzle Table into a plain object
*
* If `mode` is 'insert', the schema will be refined for insert
* If `mode` is 'select', the schema will be refined for select
* If `mode` is undefined, the schema will be spread as is, models will need to be refined manually
*/
export const spreads = <
T extends Record<string, TObject | Table>,
Mode extends 'select' | 'insert' | undefined,
>(
models: T,
mode?: Mode,
): {
[K in keyof T]: Spread<T[K], Mode>
} => {
const newSchema: Record<string, unknown> = {}
const keys = Object.keys(models)
for (const key of keys) newSchema[key] = spread(models[key], mode)
return newSchema as any
}
```
Usage:
```typescript
// ✅ Using spread
const user = spread(table.user, 'insert')
const createUser = t.Object({
id: user.id,
username: user.username,
password: user.password
})
// ⚠️ Using t.Pick
const _createUser = createInsertSchema(table.user)
const createUser = t.Pick(_createUser, ['id', 'username', 'password'])
```
## Table Singleton Pattern
```typescript
// src/database/model.ts
import { table } from './schema'
import { spreads } from './utils'
export const db = {
insert: spreads({ user: table.user }, 'insert'),
select: spreads({ user: table.user }, 'select')
} as const
```
Usage:
```typescript
// src/index.ts
import { db } from './database/model'
const { user } = db.insert
new Elysia()
.post('/sign-up', ({ body }) => {}, {
body: t.Object({
id: user.username,
username: user.username,
password: user.password
})
})
```
## Refinement
```typescript
// src/database/model.ts
import { createInsertSchema, createSelectSchema } from 'drizzle-typebox'
export const db = {
insert: spreads({
user: createInsertSchema(table.user, {
email: t.String({ format: 'email' })
})
}, 'insert'),
select: spreads({
user: createSelectSchema(table.user, {
email: t.String({ format: 'email' })
})
}, 'select')
} as const
```
`spread` skips refined schemas.

View File

@@ -0,0 +1,95 @@
# Expo Integration
Run Elysia on Expo (React Native)
## What It Is
Create API routes in Expo app (SDK 50+, App Router v3).
## Setup
1. Create `app/[...slugs]+api.ts`
2. Define Elysia server
3. Export `Elysia.fetch` as HTTP methods
```typescript
// app/[...slugs]+api.ts
import { Elysia, t } from 'elysia'
const app = new Elysia()
.get('/', 'hello Expo')
.post('/', ({ body }) => body, {
body: t.Object({ name: t.String() })
})
export const GET = app.fetch
export const POST = app.fetch
```
## Prefix for Non-Root
If placed in `app/api/[...slugs]+api.ts`, set prefix:
```typescript
const app = new Elysia({ prefix: '/api' })
.get('/', 'Hello Expo')
export const GET = app.fetch
export const POST = app.fetch
```
Ensures routing works in any location.
## Eden (End-to-End Type Safety)
1. Export type:
```typescript
// app/[...slugs]+api.ts
const app = new Elysia()
.get('/', 'Hello Nextjs')
.post('/user', ({ body }) => body, {
body: treaty.schema('User', { name: 'string' })
})
export type app = typeof app
export const GET = app.fetch
export const POST = app.fetch
```
2. Create client:
```typescript
// lib/eden.ts
import { treaty } from '@elysiajs/eden'
import type { app } from '../app/[...slugs]+api'
export const api = treaty<app>('localhost:3000/api')
```
3. Use in components:
```tsx
// app/page.tsx
import { api } from '../lib/eden'
export default async function Page() {
const message = await api.get()
return <h1>Hello, {message}</h1>
}
```
## Deployment
- Deploy as normal Elysia app OR
- Use experimental Expo server runtime
With Expo runtime:
```bash
expo export
# Creates dist/server/_expo/functions/[...slugs]+api.js
```
Edge function, not normal server (no port allocation).
### Adapters
- Express
- Netlify
- Vercel
## pnpm
Manual install:
```bash
pnpm add @sinclair/typebox openapi-types
```

View File

@@ -0,0 +1,103 @@
# Next.js Integration
## What It Is
Run Elysia on Next.js App Router.
## Setup
1. Create `app/api/[[...slugs]]/route.ts`
2. Define Elysia + export handlers:
```typescript
// app/api/[[...slugs]]/route.ts
import { Elysia, t } from 'elysia'
const app = new Elysia({ prefix: '/api' })
.get('/', 'Hello Nextjs')
.post('/', ({ body }) => body, {
body: t.Object({ name: t.String() })
})
export const GET = app.fetch
export const POST = app.fetch
```
WinterCG compliance - works as normal Next.js API route.
## Prefix for Non-Root
If placed in `app/user/[[...slugs]]/route.ts`, set prefix:
```typescript
const app = new Elysia({ prefix: '/user' })
.get('/', 'Hello Nextjs')
export const GET = app.fetch
export const POST = app.fetch
```
## Eden (End-to-End Type Safety)
Isomorphic fetch pattern:
- Server: Direct calls (no network)
- Client: Network calls
1. Export type:
```typescript
// app/api/[[...slugs]]/route.ts
export const app = new Elysia({ prefix: '/api' })
.get('/', 'Hello Nextjs')
.post('/user', ({ body }) => body, {
body: treaty.schema('User', { name: 'string' })
})
export type app = typeof app
export const GET = app.fetch
export const POST = app.fetch
```
2. Create client:
```typescript
// lib/eden.ts
import { treaty } from '@elysiajs/eden'
import type { app } from '../app/api/[[...slugs]]/route'
export const api =
typeof process !== 'undefined'
? treaty(app).api
: treaty<typeof app>('localhost:3000').api
```
Use `typeof process` not `typeof window` (window undefined at build time → hydration error).
3. Use in components:
```tsx
// app/page.tsx
import { api } from '../lib/eden'
export default async function Page() {
const message = await api.get()
return <h1>Hello, {message}</h1>
}
```
Works with server/client components + ISR.
## React Query
```tsx
import { useQuery } from '@tanstack/react-query'
function App() {
const { data: response } = useQuery({
queryKey: ['get'],
queryFn: () => getTreaty().get()
})
return response?.data
}
```
Works with all React Query features.
## pnpm
Manual install:
```bash
pnpm add @sinclair/typebox openapi-types
```

View File

@@ -0,0 +1,64 @@
# Node.js Integration
Run Elysia on Node.js
## What It Is
Runtime adapter to run Elysia on Node.js.
## Installation
```bash
bun add elysia @elysiajs/node
```
## Setup
Apply node adapter:
```typescript
import { Elysia } from 'elysia'
import { node } from '@elysiajs/node'
const app = new Elysia({ adapter: node() })
.get('/', () => 'Hello Elysia')
.listen(3000)
```
## Additional Setup (Recommended)
Install `tsx` for hot-reload:
```bash
bun add -d tsx @types/node typescript
```
Scripts in `package.json`:
```json
{
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc src/index.ts --outDir dist",
"start": "NODE_ENV=production node dist/index.js"
}
}
```
- **dev**: Hot-reload dev mode
- **build**: Production build
- **start**: Production server
Create `tsconfig.json`:
```bash
tsc --init
```
Update strict mode:
```json
{
"compilerOptions": {
"strict": true
}
}
```
Provides hot-reload + JSX support similar to `bun dev`.
## pnpm
Manual install:
```bash
pnpm add @sinclair/typebox openapi-types
```

View File

@@ -0,0 +1,67 @@
# Nuxt Integration
## What It Is
Community plugin `nuxt-elysia` for Nuxt API routes with Eden Treaty.
## Installation
```bash
bun add elysia @elysiajs/eden
bun add -d nuxt-elysia
```
## Setup
1. Add to Nuxt config:
```typescript
export default defineNuxtConfig({
modules: ['nuxt-elysia']
})
```
2. Create `api.ts` at project root:
```typescript
// api.ts
export default () => new Elysia()
.get('/hello', () => ({ message: 'Hello world!' }))
```
3. Use Eden Treaty:
```vue
<template>
<div>
<p>{{ data.message }}</p>
</div>
</template>
<script setup lang="ts">
const { $api } = useNuxtApp()
const { data } = await useAsyncData(async () => {
const { data, error } = await $api.hello.get()
if (error) throw new Error('Failed to call API')
return data
})
</script>
```
Auto-setup on Nuxt API route.
## Prefix
Default: `/_api`. Customize:
```typescript
export default defineNuxtConfig({
nuxtElysia: {
path: '/api'
}
})
```
Mounts on `/api` instead of `/_api`.
See [nuxt-elysia](https://github.com/tkesgar/nuxt-elysia) for more config.
## pnpm
Manual install:
```bash
pnpm add @sinclair/typebox openapi-types
```

View File

@@ -0,0 +1,93 @@
# Prisma Integration
Elysia + Prisma integration guide
## What It Is
Type-safe ORM. Generate Elysia validation models from Prisma schema via `prismabox`.
## Flow
```
Prisma → prismabox → Elysia validation → OpenAPI + Eden Treaty
```
## Installation
```bash
bun add @prisma/client prismabox && \
bun add -d prisma
```
## Prisma Schema
Add `prismabox` generator:
```prisma
// prisma/schema.prisma
generator client {
provider = "prisma-client"
output = "../generated/prisma"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
generator prismabox {
provider = "prismabox"
typeboxImportDependencyName = "elysia"
typeboxImportVariableName = "t"
inputModel = true
output = "../generated/prismabox"
}
model User {
id String @id @default(cuid())
email String @unique
name String?
posts Post[]
}
model Post {
id String @id @default(cuid())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId String
}
```
Generates:
- `User``generated/prismabox/User.ts`
- `Post``generated/prismabox/Post.ts`
## Using Generated Models
```typescript
// src/index.ts
import { Elysia, t } from 'elysia'
import { PrismaClient } from '../generated/prisma'
import { UserPlain, UserPlainInputCreate } from '../generated/prismabox/User'
const prisma = new PrismaClient()
new Elysia()
.put('/', async ({ body }) =>
prisma.user.create({ data: body }), {
body: UserPlainInputCreate,
response: UserPlain
}
)
.get('/id/:id', async ({ params: { id }, status }) => {
const user = await prisma.user.findUnique({ where: { id } })
if (!user) return status(404, 'User not found')
return user
}, {
response: {
200: UserPlain,
404: t.String()
}
})
.listen(3000)
```
Reuses DB schema in Elysia validation models.

View File

@@ -0,0 +1,134 @@
# React Email Integration
## What It Is
Use React components to create emails. Direct JSX import via Bun.
## Installation
```bash
bun add -d react-email
bun add @react-email/components react react-dom
```
Script in `package.json`:
```json
{
"scripts": {
"email": "email dev --dir src/emails"
}
}
```
Email templates → `src/emails` directory.
### TypeScript
Add to `tsconfig.json`:
```json
{
"compilerOptions": {
"jsx": "react"
}
}
```
## Email Template
```tsx
// src/emails/otp.tsx
import * as React from 'react'
import { Tailwind, Section, Text } from '@react-email/components'
export default function OTPEmail({ otp }: { otp: number }) {
return (
<Tailwind>
<Section className="flex justify-center items-center w-full min-h-screen font-sans">
<Section className="flex flex-col items-center w-76 rounded-2xl px-6 py-1 bg-gray-50">
<Text className="text-xs font-medium text-violet-500">
Verify your Email Address
</Text>
<Text className="text-gray-500 my-0">
Use the following code to verify your email address
</Text>
<Text className="text-5xl font-bold pt-2">{otp}</Text>
<Text className="text-gray-400 font-light text-xs pb-4">
This code is valid for 10 minutes
</Text>
<Text className="text-gray-600 text-xs">
Thank you for joining us
</Text>
</Section>
</Section>
</Tailwind>
)
}
OTPEmail.PreviewProps = { otp: 123456 }
```
`@react-email/components` → email-client compatible (Gmail, Outlook). Tailwind support.
`PreviewProps` → playground only.
## Preview
```bash
bun email
```
Opens browser with preview.
## Send Email
Render with `react-dom/server`, submit via provider:
### Nodemailer
```typescript
import { renderToStaticMarkup } from 'react-dom/server'
import OTPEmail from './emails/otp'
import nodemailer from 'nodemailer'
const transporter = nodemailer.createTransport({
host: 'smtp.gehenna.sh',
port: 465,
auth: { user: 'makoto', pass: '12345678' }
})
.get('/otp', async ({ body }) => {
const otp = ~~(Math.random() * 900_000) + 100_000
const html = renderToStaticMarkup(<OTPEmail otp={otp} />)
await transporter.sendMail({
from: '[email protected]',
to: body,
subject: 'Verify your email address',
html
})
return { success: true }
}, {
body: t.String({ format: 'email' })
})
```
### Resend
```typescript
import OTPEmail from './emails/otp'
import Resend from 'resend'
const resend = new Resend('re_123456789')
.get('/otp', ({ body }) => {
const otp = ~~(Math.random() * 900_000) + 100_000
await resend.emails.send({
from: '[email protected]',
to: body,
subject: 'Verify your email address',
html: <OTPEmail otp={otp} /> // Direct JSX
})
return { success: true }
})
```
Direct JSX import thanks to Bun.
Other providers: AWS SES, SendGrid.
See [React Email Integrations](https://react.email/docs/integrations/overview).

View File

@@ -0,0 +1,53 @@
# SvelteKit Integration
## What It Is
Run Elysia on SvelteKit server routes.
## Setup
1. Create `src/routes/[...slugs]/+server.ts`
2. Define Elysia server
3. Export fallback handler:
```typescript
// src/routes/[...slugs]/+server.ts
import { Elysia, t } from 'elysia'
const app = new Elysia()
.get('/', 'hello SvelteKit')
.post('/', ({ body }) => body, {
body: t.Object({ name: t.String() })
})
interface WithRequest {
request: Request
}
export const fallback = ({ request }: WithRequest) => app.handle(request)
```
Treat as normal SvelteKit server route.
## Prefix for Non-Root
If placed in `src/routes/api/[...slugs]/+server.ts`, set prefix:
```typescript
// src/routes/api/[...slugs]/+server.ts
import { Elysia, t } from 'elysia'
const app = new Elysia({ prefix: '/api' })
.get('/', () => 'hi')
.post('/', ({ body }) => body, {
body: t.Object({ name: t.String() })
})
type RequestHandler = (v: { request: Request }) => Response | Promise<Response>
export const fallback: RequestHandler = ({ request }) => app.handle(request)
```
Ensures routing works in any location.
## pnpm
Manual install:
```bash
pnpm add @sinclair/typebox openapi-types
```

View File

@@ -0,0 +1,87 @@
# Tanstack Start Integration
## What It Is
Elysia runs inside Tanstack Start server routes.
## Setup
1. Create `src/routes/api.$.ts`
2. Define Elysia server
3. Export handlers in `server.handlers`:
```typescript
// src/routes/api.$.ts
import { Elysia } from 'elysia'
import { createFileRoute } from '@tanstack/react-router'
import { createIsomorphicFn } from '@tanstack/react-start'
const app = new Elysia({
prefix: '/api'
}).get('/', 'Hello Elysia!')
const handle = ({ request }: { request: Request }) => app.fetch(request)
export const Route = createFileRoute('/api/$')({
server: {
handlers: {
GET: handle,
POST: handle
}
}
})
```
Runs on `/api`. Add methods to `server.handlers` as needed.
## Eden (End-to-End Type Safety)
Isomorphic pattern with `createIsomorphicFn`:
```typescript
// src/routes/api.$.ts
export const getTreaty = createIsomorphicFn()
.server(() => treaty(app).api)
.client(() => treaty<typeof app>('localhost:3000').api)
```
- Server: Direct call (no HTTP overhead)
- Client: HTTP call
## Loader Data
Fetch before render:
```tsx
// src/routes/index.tsx
import { createFileRoute } from '@tanstack/react-router'
import { getTreaty } from './api.$'
export const Route = createFileRoute('/a')({
component: App,
loader: () => getTreaty().get().then((res) => res.data)
})
function App() {
const data = Route.useLoaderData()
return data
}
```
Executed server-side during SSR. No HTTP overhead. Type-safe.
## React Query
```tsx
import { useQuery } from '@tanstack/react-query'
import { getTreaty } from './api.$'
function App() {
const { data: response } = useQuery({
queryKey: ['get'],
queryFn: () => getTreaty().get()
})
return response?.data
}
```
Works with all React Query features.
## pnpm
Manual install:
```bash
pnpm add @sinclair/typebox openapi-types
```

View File

@@ -0,0 +1,55 @@
# Vercel Integration
Deploy Elysia on Vercel
## What It Is
Zero-config deployment on Vercel (Bun or Node runtime).
## Setup
1. Create/import Elysia server in `src/index.ts`
2. Export as default:
```typescript
import { Elysia, t } from 'elysia'
export default new Elysia()
.get('/', () => 'Hello Vercel Function')
.post('/', ({ body }) => body, {
body: t.Object({ name: t.String() })
})
```
3. Develop locally:
```bash
vc dev
```
4. Deploy:
```bash
vc deploy
```
## Node.js Runtime
Set in `package.json`:
```json
{
"name": "elysia-app",
"type": "module"
}
```
## Bun Runtime
Set in `vercel.json`:
```json
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"bunVersion": "1.x"
}
```
## pnpm
Manual install:
```bash
pnpm add @sinclair/typebox openapi-types
```
## Troubleshooting
Vercel has zero config for Elysia. For additional config, see [Vercel docs](https://vercel.com/docs/frameworks/backend/elysia).

View File

@@ -0,0 +1,380 @@
# MVC pattern
This file contains a guideline for using Elysia with MVC or Model View Controller patterns
- Controller:
- Prefers Elysia as a controller for HTTP dependant
- For non HTTP dependent, prefers service instead unless explicitly asked
- Use `onError` to handle local custom errors
- Register Model to Elysia instance via `Elysia.models({ ...models })` and prefix model by namespace `Elysia.prefix('model', 'Namespace.')
- Prefers Reference Model by name provided by Elysia instead of using an actual `Model.name`
- Service:
- Prefers class (or abstract class if possible)
- Prefers interface/type derive from `Model`
- Return `status` (`import { status } from 'elysia'`) for error
- Prefers `return Error` instead of `throw Error`
- Models:
- Always export validation model and type of validation model
- Custom Error should be in contains in Model
## Controller
Due to type soundness of Elysia, it's not recommended to use a traditional controller class that is tightly coupled with Elysia's `Context` because:
1. **Elysia type is complex** and heavily depends on plugin and multiple level of chaining.
2. **Hard to type**, Elysia type could change at anytime, especially with decorators, and store
3. **Loss of type integrity**, and inconsistency between types and runtime code.
We recommended one of the following approach to implement a controller in Elysia.
1. Use Elysia instance as a controller itself
2. Create a controller that is not tied with HTTP request or Elysia.
---
### 1. Elysia instance as a controller
> 1 Elysia instance = 1 controller
Treat an Elysia instance as a controller, and define your routes directly on the Elysia instance.
```typescript
// Do
import { Elysia } from 'elysia'
import { Service } from './service'
new Elysia()
.get('/', ({ stuff }) => {
Service.doStuff(stuff)
})
```
This approach allows Elysia to infer the `Context` type automatically, ensuring type integrity and consistency between types and runtime code.
```typescript
// Don't
import { Elysia, t, type Context } from 'elysia'
abstract class Controller {
static root(context: Context) {
return Service.doStuff(context.stuff)
}
}
new Elysia()
.get('/', Controller.root)
```
This approach makes it hard to type `Context` properly, and may lead to loss of type integrity.
### 2. Controller without HTTP request
If you want to create a controller class, we recommend creating a class that is not tied to HTTP request or Elysia at all.
This approach allows you to decouple the controller from Elysia, making it easier to test, reuse, and even swap a framework while still follows the MVC pattern.
```typescript
import { Elysia } from 'elysia'
abstract class Controller {
static doStuff(stuff: string) {
return Service.doStuff(stuff)
}
}
new Elysia()
.get('/', ({ stuff }) => Controller.doStuff(stuff))
```
Tying the controller to Elysia Context may lead to:
1. Loss of type integrity
2. Make it harder to test and reuse
3. Lead to vendor lock-in
We recommended to keep the controller decoupled from Elysia as much as possible.
### Don't: Pass entire `Context` to a controller
**Context is a highly dynamic type** that can be inferred from Elysia instance.
Do not pass an entire `Context` to a controller, instead use object destructuring to extract what you need and pass it to the controller.
```typescript
import type { Context } from 'elysia'
abstract class Controller {
constructor() {}
// Don't do this
static root(context: Context) {
return Service.doStuff(context.stuff)
}
}
```
This approach makes it hard to type `Context` properly, and may lead to loss of type integrity.
### Testing
If you're using Elysia as a controller, you can test your controller using `handle` to directly call a function (and it's lifecycle)
```typescript
import { Elysia } from 'elysia'
import { Service } from './service'
import { describe, it, expect } from 'bun:test'
const app = new Elysia()
.get('/', ({ stuff }) => {
Service.doStuff(stuff)
return 'ok'
})
describe('Controller', () => {
it('should work', async () => {
const response = await app
.handle(new Request('http://localhost/'))
.then((x) => x.text())
expect(response).toBe('ok')
})
})
```
You may find more information about testing in [Unit Test](/patterns/unit-test.html).
## Service
Service is a set of utility/helper functions decoupled as a business logic to use in a module/controller, in our case, an Elysia instance.
Any technical logic that can be decoupled from controller may live inside a **Service**.
There are 2 types of service in Elysia:
1. Non-request dependent service
2. Request dependent service
### 1. Abstract away Non-request dependent service
We recommend abstracting a service class/function away from Elysia.
If the service or function isn't tied to an HTTP request or doesn't access a `Context`, it's recommended to implement it as a static class or function.
```typescript
import { Elysia, t } from 'elysia'
abstract class Service {
static fibo(number: number): number {
if(number < 2)
return number
return Service.fibo(number - 1) + Service.fibo(number - 2)
}
}
new Elysia()
.get('/fibo', ({ body }) => {
return Service.fibo(body)
}, {
body: t.Numeric()
})
```
If your service doesn't need to store a property, you may use `abstract class` and `static` instead to avoid allocating class instance.
### 2. Request dependent service as Elysia instance
**If the service is a request-dependent service** or needs to process HTTP requests, we recommend abstracting it as an Elysia instance to ensure type integrity and inference:
```typescript
import { Elysia } from 'elysia'
// Do
const AuthService = new Elysia({ name: 'Auth.Service' })
.macro({
isSignIn: {
resolve({ cookie, status }) {
if (!cookie.session.value) return status(401)
return {
session: cookie.session.value,
}
}
}
})
const UserController = new Elysia()
.use(AuthService)
.get('/profile', ({ Auth: { user } }) => user, {
isSignIn: true
})
```
### Do: Decorate only request dependent property
It's recommended to `decorate` only request-dependent properties, such as `requestIP`, `requestTime`, or `session`.
Overusing decorators may tie your code to Elysia, making it harder to test and reuse.
```typescript
import { Elysia } from 'elysia'
new Elysia()
.decorate('requestIP', ({ request }) => request.headers.get('x-forwarded-for') || request.ip)
.decorate('requestTime', () => Date.now())
.decorate('session', ({ cookie }) => cookie.session.value)
.get('/', ({ requestIP, requestTime, session }) => {
return { requestIP, requestTime, session }
})
```
### Don't: Pass entire `Context` to a service
**Context is a highly dynamic type** that can be inferred from Elysia instance.
Do not pass an entire `Context` to a service, instead use object destructuring to extract what you need and pass it to the service.
```typescript
import type { Context } from 'elysia'
class AuthService {
constructor() {}
// Don't do this
isSignIn({ status, cookie: { session } }: Context) {
if (session.value)
return status(401)
}
}
```
As Elysia type is complex, and heavily depends on plugin and multiple level of chaining, it can be challenging to manually type as it's highly dynamic.
## Model
Model or [DTO (Data Transfer Object)](https://en.wikipedia.org/wiki/Data_transfer_object) is handle by [Elysia.t (Validation)](/essential/validation.html#elysia-type).
Elysia has a validation system built-in which can infers type from your code and validate it at runtime.
### Do: Use Elysia's validation system
Elysia strength is prioritizing a single source of truth for both type and runtime validation.
Instead of declaring an interface, reuse validation's model instead:
```typescript twoslash
// Do
import { Elysia, t } from 'elysia'
const customBody = t.Object({
username: t.String(),
password: t.String()
})
// Optional if you want to get the type of the model
// Usually if we didn't use the type, as it's already inferred by Elysia
type CustomBody = typeof customBody.static
export { customBody }
```
We can get type of model by using `typeof` with `.static` property from the model.
Then you can use the `CustomBody` type to infer the type of the request body.
```typescript twoslash
// Do
new Elysia()
.post('/login', ({ body }) => {
return body
}, {
body: customBody
})
```
### Don't: Declare a class instance as a model
Do not declare a class instance as a model:
```typescript
// Don't
class CustomBody {
username: string
password: string
constructor(username: string, password: string) {
this.username = username
this.password = password
}
}
// Don't
interface ICustomBody {
username: string
password: string
}
```
### Don't: Declare type separate from the model
Do not declare a type separate from the model, instead use `typeof` with `.static` property to get the type of the model.
```typescript
// Don't
import { Elysia, t } from 'elysia'
const customBody = t.Object({
username: t.String(),
password: t.String()
})
type CustomBody = {
username: string
password: string
}
// Do
const customBody = t.Object({
username: t.String(),
password: t.String()
})
type CustomBody = typeof customBody.static
```
### Group
You can group multiple models into a single object to make it more organized.
```typescript
import { Elysia, t } from 'elysia'
export const AuthModel = {
sign: t.Object({
username: t.String(),
password: t.String()
})
}
const models = AuthModel.models
```
### Model Injection
Though this is optional, if you are strictly following MVC pattern, you may want to inject like a service into a controller. We recommended using Elysia reference model
Using Elysia's model reference
```typescript twoslash
import { Elysia, t } from 'elysia'
const customBody = t.Object({
username: t.String(),
password: t.String()
})
const AuthModel = new Elysia()
.model({
sign: customBody
})
const models = AuthModel.models
const UserController = new Elysia({ prefix: '/auth' })
.use(AuthModel)
.prefix('model', 'auth.')
.post('/sign-in', async ({ body, cookie: { session } }) => {
return true
}, {
body: 'auth.Sign'
})
```
This approach provide several benefits:
1. Allow us to name a model and provide auto-completion.
2. Modify schema for later usage, or perform a [remap](/essential/handler.html#remap).
3. Show up as "models" in OpenAPI compliance client, eg. OpenAPI.
4. Improve TypeScript inference speed as model type will be cached during registration.

View File

@@ -0,0 +1,30 @@
# Bearer
Plugin for Elysia for retrieving the Bearer token.
## Installation
```bash
bun add @elysiajs/bearer
```
## Basic Usage
```typescript twoslash
import { Elysia } from 'elysia'
import { bearer } from '@elysiajs/bearer'
const app = new Elysia()
.use(bearer())
.get('/sign', ({ bearer }) => bearer, {
beforeHandle({ bearer, set, status }) {
if (!bearer) {
set.headers[
'WWW-Authenticate'
] = `Bearer realm='sign', error="invalid_request"`
return status(400, 'Unauthorized')
}
}
})
.listen(3000)
```
This plugin is for retrieving a Bearer token specified in RFC6750

View File

@@ -0,0 +1,141 @@
# CORS
Plugin for Elysia that adds support for customizing Cross-Origin Resource Sharing behavior.
## Installation
```bash
bun add @elysiajs/cors
```
## Basic Usage
```typescript twoslash
import { Elysia } from 'elysia'
import { cors } from '@elysiajs/cors'
new Elysia().use(cors()).listen(3000)
```
This will set Elysia to accept requests from any origin.
## Config
Below is a config which is accepted by the plugin
### origin
@default `true`
Indicates whether the response can be shared with the requesting code from the given origins.
Value can be one of the following:
- **string** - Name of origin which will directly assign to Access-Control-Allow-Origin header.
- **boolean** - If set to true, Access-Control-Allow-Origin will be set to `*` (any origins)
- **RegExp** - Pattern to match request's URL, allowed if matched.
- **Function** - Custom logic to allow resource sharing, allow if `true` is returned.
- Expected to have the type of:
```typescript
cors(context: Context) => boolean | void
```
- **Array<string | RegExp | Function>** - iterate through all cases above in order, allowed if any of the values are `true`.
---
### methods
@default `*`
Allowed methods for cross-origin requests by assign `Access-Control-Allow-Methods` header.
Value can be one of the following:
- **undefined | null | ''** - Ignore all methods.
- **\*** - Allows all methods.
- **string** - Expects either a single method or a comma-delimited string
- (eg: `'GET, PUT, POST'`)
- **string[]** - Allow multiple HTTP methods.
- eg: `['GET', 'PUT', 'POST']`
---
### allowedHeaders
@default `*`
Allowed headers for an incoming request by assign `Access-Control-Allow-Headers` header.
Value can be one of the following:
- **string** - Expects either a single header or a comma-delimited string
- eg: `'Content-Type, Authorization'`.
- **string[]** - Allow multiple HTTP headers.
- eg: `['Content-Type', 'Authorization']`
---
### exposeHeaders
@default `*`
Response CORS with specified headers by sssign Access-Control-Expose-Headers header.
Value can be one of the following:
- **string** - Expects either a single header or a comma-delimited string.
- eg: `'Content-Type, X-Powered-By'`.
- **string[]** - Allow multiple HTTP headers.
- eg: `['Content-Type', 'X-Powered-By']`
---
### credentials
@default `true`
The Access-Control-Allow-Credentials response header tells browsers whether to expose the response to the frontend JavaScript code when the request's credentials mode Request.credentials is `include`.
Credentials are cookies, authorization headers, or TLS client certificates by assign `Access-Control-Allow-Credentials` header.
---
### maxAge
@default `5`
Indicates how long the results of a preflight request that is the information contained in the `Access-Control-Allow-Methods` and `Access-Control-Allow-Headers` headers) can be cached.
Assign `Access-Control-Max-Age` header.
---
### preflight
The preflight request is a request sent to check if the CORS protocol is understood and if a server is aware of using specific methods and headers.
Response with **OPTIONS** request with 3 HTTP request headers:
- **Access-Control-Request-Method**
- **Access-Control-Request-Headers**
- **Origin**
This config indicates if the server should respond to preflight requests.
---
## Pattern
Below you can find the common patterns to use the plugin.
## Allow CORS by top-level domain
```typescript twoslash
import { Elysia } from 'elysia'
import { cors } from '@elysiajs/cors'
const app = new Elysia()
.use(
cors({
origin: /.*\.saltyaom\.com$/
})
)
.get('/', () => 'Hi')
.listen(3000)
```
This will allow requests from top-level domains with `saltyaom.com`

View File

@@ -0,0 +1,265 @@
# Cron Plugin
This plugin adds support for running cronjob to Elysia server.
## Installation
```bash
bun add @elysiajs/cron
```
## Basic Usage
```typescript twoslash
import { Elysia } from 'elysia'
import { cron } from '@elysiajs/cron'
new Elysia()
.use(
cron({
name: 'heartbeat',
pattern: '*/10 * * * * *',
run() {
console.log('Heartbeat')
}
})
)
.listen(3000)
```
The above code will log `heartbeat` every 10 seconds.
## Config
Below is a config which is accepted by the plugin
### cron
Create a cronjob for the Elysia server.
```
cron(config: CronConfig, callback: (Instance['store']) => void): this
```
`CronConfig` accepts the parameters specified below:
---
### CronConfig.name
Job name to register to `store`.
This will register the cron instance to `store` with a specified name, which can be used to reference in later processes eg. stop the job.
---
### CronConfig.pattern
Time to run the job as specified by cron syntax.
```
┌────────────── second (optional)
│ ┌──────────── minute
│ │ ┌────────── hour
│ │ │ ┌──────── day of the month
│ │ │ │ ┌────── month
│ │ │ │ │ ┌──── day of week
│ │ │ │ │ │
* * * * * *
```
---
### CronConfig.timezone
Time zone in Europe/Stockholm format
---
### CronConfig.startAt
Schedule start time for the job
---
### CronConfig.stopAt
Schedule stop time for the job
---
### CronConfig.maxRuns
Maximum number of executions
---
### CronConfig.catch
Continue execution even if an unhandled error is thrown by a triggered function.
### CronConfig.interval
The minimum interval between executions, in seconds.
---
## CronConfig.Pattern
Below you can find the common patterns to use the plugin.
---
## Pattern
Below you can find the common patterns to use the plugin.
## Stop cronjob
You can stop cronjob manually by accessing the cronjob name registered to `store`.
```typescript
import { Elysia } from 'elysia'
import { cron } from '@elysiajs/cron'
const app = new Elysia()
.use(
cron({
name: 'heartbeat',
pattern: '*/1 * * * * *',
run() {
console.log('Heartbeat')
}
})
)
.get(
'/stop',
({
store: {
cron: { heartbeat }
}
}) => {
heartbeat.stop()
return 'Stop heartbeat'
}
)
.listen(3000)
```
---
## Predefined patterns
You can use predefined patterns from `@elysiajs/cron/schedule`
```typescript
import { Elysia } from 'elysia'
import { cron, Patterns } from '@elysiajs/cron'
const app = new Elysia()
.use(
cron({
name: 'heartbeat',
pattern: Patterns.everySecond(),
run() {
console.log('Heartbeat')
}
})
)
.get(
'/stop',
({
store: {
cron: { heartbeat }
}
}) => {
heartbeat.stop()
return 'Stop heartbeat'
}
)
.listen(3000)
```
### Functions
| Function | Description |
| ---------------------------------------- | ----------------------------------------------------- |
| `.everySeconds(2)` | Run the task every 2 seconds |
| `.everyMinutes(5)` | Run the task every 5 minutes |
| `.everyHours(3)` | Run the task every 3 hours |
| `.everyHoursAt(3, 15)` | Run the task every 3 hours at 15 minutes |
| `.everyDayAt('04:19')` | Run the task every day at 04:19 |
| `.everyWeekOn(Patterns.MONDAY, '19:30')` | Run the task every Monday at 19:30 |
| `.everyWeekdayAt('17:00')` | Run the task every day from Monday to Friday at 17:00 |
| `.everyWeekendAt('11:00')` | Run the task on Saturday and Sunday at 11:00 |
### Function aliases to constants
| Function | Constant |
| ----------------- | ---------------------------------- |
| `.everySecond()` | EVERY_SECOND |
| `.everyMinute()` | EVERY_MINUTE |
| `.hourly()` | EVERY_HOUR |
| `.daily()` | EVERY_DAY_AT_MIDNIGHT |
| `.everyWeekday()` | EVERY_WEEKDAY |
| `.everyWeekend()` | EVERY_WEEKEND |
| `.weekly()` | EVERY_WEEK |
| `.monthly()` | EVERY_1ST_DAY_OF_MONTH_AT_MIDNIGHT |
| `.everyQuarter()` | EVERY_QUARTER |
| `.yearly()` | EVERY_YEAR |
### Constants
| Constant | Pattern |
| ---------------------------------------- | -------------------- |
| `.EVERY_SECOND` | `* * * * * *` |
| `.EVERY_5_SECONDS` | `*/5 * * * * *` |
| `.EVERY_10_SECONDS` | `*/10 * * * * *` |
| `.EVERY_30_SECONDS` | `*/30 * * * * *` |
| `.EVERY_MINUTE` | `*/1 * * * *` |
| `.EVERY_5_MINUTES` | `0 */5 * * * *` |
| `.EVERY_10_MINUTES` | `0 */10 * * * *` |
| `.EVERY_30_MINUTES` | `0 */30 * * * *` |
| `.EVERY_HOUR` | `0 0-23/1 * * *` |
| `.EVERY_2_HOURS` | `0 0-23/2 * * *` |
| `.EVERY_3_HOURS` | `0 0-23/3 * * *` |
| `.EVERY_4_HOURS` | `0 0-23/4 * * *` |
| `.EVERY_5_HOURS` | `0 0-23/5 * * *` |
| `.EVERY_6_HOURS` | `0 0-23/6 * * *` |
| `.EVERY_7_HOURS` | `0 0-23/7 * * *` |
| `.EVERY_8_HOURS` | `0 0-23/8 * * *` |
| `.EVERY_9_HOURS` | `0 0-23/9 * * *` |
| `.EVERY_10_HOURS` | `0 0-23/10 * * *` |
| `.EVERY_11_HOURS` | `0 0-23/11 * * *` |
| `.EVERY_12_HOURS` | `0 0-23/12 * * *` |
| `.EVERY_DAY_AT_1AM` | `0 01 * * *` |
| `.EVERY_DAY_AT_2AM` | `0 02 * * *` |
| `.EVERY_DAY_AT_3AM` | `0 03 * * *` |
| `.EVERY_DAY_AT_4AM` | `0 04 * * *` |
| `.EVERY_DAY_AT_5AM` | `0 05 * * *` |
| `.EVERY_DAY_AT_6AM` | `0 06 * * *` |
| `.EVERY_DAY_AT_7AM` | `0 07 * * *` |
| `.EVERY_DAY_AT_8AM` | `0 08 * * *` |
| `.EVERY_DAY_AT_9AM` | `0 09 * * *` |
| `.EVERY_DAY_AT_10AM` | `0 10 * * *` |
| `.EVERY_DAY_AT_11AM` | `0 11 * * *` |
| `.EVERY_DAY_AT_NOON` | `0 12 * * *` |
| `.EVERY_DAY_AT_1PM` | `0 13 * * *` |
| `.EVERY_DAY_AT_2PM` | `0 14 * * *` |
| `.EVERY_DAY_AT_3PM` | `0 15 * * *` |
| `.EVERY_DAY_AT_4PM` | `0 16 * * *` |
| `.EVERY_DAY_AT_5PM` | `0 17 * * *` |
| `.EVERY_DAY_AT_6PM` | `0 18 * * *` |
| `.EVERY_DAY_AT_7PM` | `0 19 * * *` |
| `.EVERY_DAY_AT_8PM` | `0 20 * * *` |
| `.EVERY_DAY_AT_9PM` | `0 21 * * *` |
| `.EVERY_DAY_AT_10PM` | `0 22 * * *` |
| `.EVERY_DAY_AT_11PM` | `0 23 * * *` |
| `.EVERY_DAY_AT_MIDNIGHT` | `0 0 * * *` |
| `.EVERY_WEEK` | `0 0 * * 0` |
| `.EVERY_WEEKDAY` | `0 0 * * 1-5` |
| `.EVERY_WEEKEND` | `0 0 * * 6,0` |
| `.EVERY_1ST_DAY_OF_MONTH_AT_MIDNIGHT` | `0 0 1 * *` |
| `.EVERY_1ST_DAY_OF_MONTH_AT_NOON` | `0 12 1 * *` |
| `.EVERY_2ND_HOUR` | `0 */2 * * *` |
| `.EVERY_2ND_HOUR_FROM_1AM_THROUGH_11PM` | `0 1-23/2 * * *` |
| `.EVERY_2ND_MONTH` | `0 0 1 */2 *` |
| `.EVERY_QUARTER` | `0 0 1 */3 *` |
| `.EVERY_6_MONTHS` | `0 0 1 */6 *` |
| `.EVERY_YEAR` | `0 0 1 1 *` |
| `.EVERY_30_MINUTES_BETWEEN_9AM_AND_5PM` | `0 */30 9-17 * * *` |
| `.EVERY_30_MINUTES_BETWEEN_9AM_AND_6PM` | `0 */30 9-18 * * *` |
| `.EVERY_30_MINUTES_BETWEEN_10AM_AND_7PM` | `0 */30 10-19 * * *` |

View File

@@ -0,0 +1,90 @@
# GraphQL Apollo
Plugin for Elysia to use GraphQL Apollo.
## Installation
```bash
bun add graphql @elysiajs/apollo @apollo/server
```
## Basic Usage
```typescript
import { Elysia } from 'elysia'
import { apollo, gql } from '@elysiajs/apollo'
const app = new Elysia()
.use(
apollo({
typeDefs: gql`
type Book {
title: String
author: String
}
type Query {
books: [Book]
}
`,
resolvers: {
Query: {
books: () => {
return [
{
title: 'Elysia',
author: 'saltyAom'
}
]
}
}
}
})
)
.listen(3000)
```
Accessing `/graphql` should show Apollo GraphQL playground work with.
## Context
Because Elysia is based on Web Standard Request and Response which is different from Node's `HttpRequest` and `HttpResponse` that Express uses, results in `req, res` being undefined in context.
Because of this, Elysia replaces both with `context` like route parameters.
```typescript
const app = new Elysia()
.use(
apollo({
typeDefs,
resolvers,
context: async ({ request }) => {
const authorization = request.headers.get('Authorization')
return {
authorization
}
}
})
)
.listen(3000)
```
## Config
This plugin extends Apollo's [ServerRegistration](https://www.apollographql.com/docs/apollo-server/api/apollo-server/#options) (which is `ApolloServer`'s' constructor parameter).
Below are the extended parameters for configuring Apollo Server with Elysia.
### path
@default `"/graphql"`
Path to expose Apollo Server.
---
### enablePlayground
@default `process.env.ENV !== 'production'`
Determine whether should Apollo should provide Apollo Playground.

View File

@@ -0,0 +1,87 @@
# GraphQL Yoga
This plugin integrates GraphQL yoga with Elysia
## Installation
```bash
bun add @elysiajs/graphql-yoga
```
## Basic Usage
```typescript
import { Elysia } from 'elysia'
import { yoga } from '@elysiajs/graphql-yoga'
const app = new Elysia()
.use(
yoga({
typeDefs: /* GraphQL */ `
type Query {
hi: String
}
`,
resolvers: {
Query: {
hi: () => 'Hello from Elysia'
}
}
})
)
.listen(3000)
```
Accessing `/graphql` in the browser (GET request) would show you a GraphiQL instance for the GraphQL-enabled Elysia server.
optional: you can install a custom version of optional peer dependencies as well:
```bash
bun add graphql graphql-yoga
```
## Resolver
Elysia uses Mobius to infer type from **typeDefs** field automatically, allowing you to get full type-safety and auto-complete when typing **resolver** types.
## Context
You can add custom context to the resolver function by adding **context**
```ts
import { Elysia } from 'elysia'
import { yoga } from '@elysiajs/graphql-yoga'
const app = new Elysia()
.use(
yoga({
typeDefs: /* GraphQL */ `
type Query {
hi: String
}
`,
context: {
name: 'Mobius'
},
// If context is a function on this doesn't present
// for some reason it won't infer context type
useContext(_) {},
resolvers: {
Query: {
hi: async (parent, args, context) => context.name
}
}
})
)
.listen(3000)
```
## Config
This plugin extends [GraphQL Yoga's createYoga options, please refer to the GraphQL Yoga documentation](https://the-guild.dev/graphql/yoga-server/docs) with inlining `schema` config to root.
Below is a config which is accepted by the plugin
### path
@default `/graphql`
Endpoint to expose GraphQL handler

View File

@@ -0,0 +1,188 @@
# HTML
Allows you to use JSX and HTML with proper headers and support.
## Installation
```bash
bun add @elysiajs/html
```
## Basic Usage
```tsx twoslash
import React from 'react'
import { Elysia } from 'elysia'
import { html, Html } from '@elysiajs/html'
new Elysia()
.use(html())
.get(
'/html',
() => `
<html lang='en'>
<head>
<title>Hello World</title>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>`
)
.get('/jsx', () => (
<html lang="en">
<head>
<title>Hello World</title>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
))
.listen(3000)
```
This plugin will automatically add `Content-Type: text/html; charset=utf8` header to the response, add `<!doctype html>`, and convert it into a Response object.
## JSX
Elysia can use JSX
1. Replace your file that needs to use JSX to end with affix **"x"**:
- .js -> .jsx
- .ts -> .tsx
2. Register the TypeScript type by append the following to **tsconfig.json**:
```jsonc
// tsconfig.json
{
"compilerOptions": {
"jsx": "react",
"jsxFactory": "Html.createElement",
"jsxFragmentFactory": "Html.Fragment"
}
}
```
3. Starts using JSX in your file
```tsx twoslash
import React from 'react'
import { Elysia } from 'elysia'
import { html, Html } from '@elysiajs/html'
new Elysia()
.use(html())
.get('/', () => (
<html lang="en">
<head>
<title>Hello World</title>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
))
.listen(3000)
```
If the error `Cannot find name 'Html'. Did you mean 'html'?` occurs, this import must be added to the JSX template:
```tsx
import { Html } from '@elysiajs/html'
```
It is important that it is written in uppercase.
## XSS
Elysia HTML is based use of the Kita HTML plugin to detect possible XSS attacks in compile time.
You can use a dedicated `safe` attribute to sanitize user value to prevent XSS vulnerability.
```tsx
import { Elysia, t } from 'elysia'
import { html, Html } from '@elysiajs/html'
new Elysia()
.use(html())
.post(
'/',
({ body }) => (
<html lang="en">
<head>
<title>Hello World</title>
</head>
<body>
<h1 safe>{body}</h1>
</body>
</html>
),
{
body: t.String()
}
)
.listen(3000)
```
However, when are building a large-scale app, it's best to have a type reminder to detect possible XSS vulnerabilities in your codebase.
To add a type-safe reminder, please install:
```sh
bun add @kitajs/ts-html-plugin
```
Then appends the following **tsconfig.json**
```jsonc
// tsconfig.json
{
"compilerOptions": {
"jsx": "react",
"jsxFactory": "Html.createElement",
"jsxFragmentFactory": "Html.Fragment",
"plugins": [{ "name": "@kitajs/ts-html-plugin" }]
}
}
```
## Config
Below is a config which is accepted by the plugin
### contentType
- Type: `string`
- Default: `'text/html; charset=utf8'`
The content-type of the response.
### autoDetect
- Type: `boolean`
- Default: `true`
Whether to automatically detect HTML content and set the content-type.
### autoDoctype
- Type: `boolean | 'full'`
- Default: `true`
Whether to automatically add `<!doctype html>` to a response starting with `<html>`, if not found.
Use `full` to also automatically add doctypes on responses returned without this plugin
```ts
// without the plugin
app.get('/', () => '<html></html>')
// With the plugin
app.get('/', ({ html }) => html('<html></html>'))
```
### isHtml
- Type: `(value: string) => boolean`
- Default: `isHtml` (exported function)
The function is used to detect if a string is a html or not. Default implementation if length is greater than 7, starts with `<` and ends with `>`.
Keep in mind there's no real way to validate HTML, so the default implementation is a best guess.

View File

@@ -0,0 +1,197 @@
# JWT Plugin
This plugin adds support for using JWT in Elysia handlers.
## Installation
```bash
bun add @elysiajs/jwt
```
## Basic Usage
```typescript [cookie]
import { Elysia } from 'elysia'
import { jwt } from '@elysiajs/jwt'
const app = new Elysia()
.use(
jwt({
name: 'jwt',
secret: 'Fischl von Luftschloss Narfidort'
})
)
.get('/sign/:name', async ({ jwt, params: { name }, cookie: { auth } }) => {
const value = await jwt.sign({ name })
auth.set({
value,
httpOnly: true,
maxAge: 7 * 86400,
path: '/profile',
})
return `Sign in as ${value}`
})
.get('/profile', async ({ jwt, status, cookie: { auth } }) => {
const profile = await jwt.verify(auth.value)
if (!profile)
return status(401, 'Unauthorized')
return `Hello ${profile.name}`
})
.listen(3000)
```
## Config
This plugin extends config from [jose](https://github.com/panva/jose).
Below is a config that is accepted by the plugin.
### name
Name to register `jwt` function as.
For example, `jwt` function will be registered with a custom name.
```typescript
new Elysia()
.use(
jwt({
name: 'myJWTNamespace',
secret: process.env.JWT_SECRETS!
})
)
.get('/sign/:name', ({ myJWTNamespace, params }) => {
return myJWTNamespace.sign(params)
})
```
Because some might need to use multiple `jwt` with different configs in a single server, explicitly registering the JWT function with a different name is needed.
### secret
The private key to sign JWT payload with.
### schema
Type strict validation for JWT payload.
### alg
@default `HS256`
Signing Algorithm to sign JWT payload with.
Possible properties for jose are:
HS256
HS384
HS512
PS256
PS384
PS512
RS256
RS384
RS512
ES256
ES256K
ES384
ES512
EdDSA
### iss
The issuer claim identifies the principal that issued the JWT as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.1)
TLDR; is usually (the domain) name of the signer.
### sub
The subject claim identifies the principal that is the subject of the JWT.
The claims in a JWT are normally statements about the subject as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.2)
### aud
The audience claim identifies the recipients that the JWT is intended for.
Each principal intended to process the JWT MUST identify itself with a value in the audience claim as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.3)
### jti
JWT ID claim provides a unique identifier for the JWT as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.7)
### nbf
The "not before" claim identifies the time before which the JWT must not be accepted for processing as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.5)
### exp
The expiration time claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.4)
### iat
The "issued at" claim identifies the time at which the JWT was issued.
This claim can be used to determine the age of the JWT as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.6)
### b64
This JWS Extension Header Parameter modifies the JWS Payload representation and the JWS Signing input computation as per [RFC7797](https://www.rfc-editor.org/rfc/rfc7797).
### kid
A hint indicating which key was used to secure the JWS.
This parameter allows originators to explicitly signal a change of key to recipients as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.4)
### x5t
(X.509 certificate SHA-1 thumbprint) header parameter is a base64url-encoded SHA-1 digest of the DER encoding of the X.509 certificate [RFC5280](https://www.rfc-editor.org/rfc/rfc5280) corresponding to the key used to digitally sign the JWS as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.7)
### x5c
(X.509 certificate chain) header parameter contains the X.509 public key certificate or certificate chain [RFC5280](https://www.rfc-editor.org/rfc/rfc5280) corresponding to the key used to digitally sign the JWS as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.6)
### x5u
(X.509 URL) header parameter is a URI [RFC3986](https://www.rfc-editor.org/rfc/rfc3986) that refers to a resource for the X.509 public key certificate or certificate chain [RFC5280] corresponding to the key used to digitally sign the JWS as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.5)
### jwk
The "jku" (JWK Set URL) Header Parameter is a URI [RFC3986] that refers to a resource for a set of JSON-encoded public keys, one of which corresponds to the key used to digitally sign the JWS.
The keys MUST be encoded as a JWK Set [JWK] as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.2)
### typ
The `typ` (type) Header Parameter is used by JWS applications to declare the media type [IANA.MediaTypes] of this complete JWS.
This is intended for use by the application when more than one kind of object could be present in an application data structure that can contain a JWS as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.9)
### ctr
Content-Type parameter is used by JWS applications to declare the media type [IANA.MediaTypes] of the secured content (the payload).
This is intended for use by the application when more than one kind of object could be present in the JWS Payload as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.9)
## Handler
Below are the value added to the handler.
### jwt.sign
A dynamic object of collection related to use with JWT registered by the JWT plugin.
Type:
```typescript
sign: (payload: JWTPayloadSpec): Promise<string>
```
`JWTPayloadSpec` accepts the same value as [JWT config](#config)
### jwt.verify
Verify payload with the provided JWT config
Type:
```typescript
verify(payload: string) => Promise<JWTPayloadSpec | false>
```
`JWTPayloadSpec` accepts the same value as [JWT config](#config)
## Pattern
Below you can find the common patterns to use the plugin.
## Set JWT expiration date
By default, the config is passed to `setCookie` and inherits its value.
```typescript
const app = new Elysia()
.use(
jwt({
name: 'jwt',
secret: 'kunikuzushi',
exp: '7d'
})
)
.get('/sign/:name', async ({ jwt, params }) => jwt.sign(params))
```
This will sign JWT with an expiration date of the next 7 days.

View File

@@ -0,0 +1,246 @@
# OpenAPI Plugin
## Installation
```bash
bun add @elysiajs/openapi
```
## Basic Usage
```typescript
import { openapi } from '@elysiajs/openapi'
new Elysia()
.use(openapi())
.get('/', () => 'hello')
```
Docs at `/openapi`, spec at `/openapi/json`.
## Detail Object
Extends OpenAPI Operation Object:
```typescript
.get('/', () => 'hello', {
detail: {
title: 'Hello',
description: 'An example route',
summary: 'Short summary',
deprecated: false,
hide: true, // Hide from docs
tags: ['App']
}
})
```
### Documentation Config
```typescript
openapi({
documentation: {
info: {
title: 'API',
version: '1.0.0'
},
tags: [
{ name: 'App', description: 'General' }
],
components: {
securitySchemes: {
bearerAuth: { type: 'http', scheme: 'bearer' }
}
}
}
})
```
### Standard Schema Mapping
```typescript
mapJsonSchema: {
zod: z.toJSONSchema, // Zod 4
valibot: toJsonSchema,
effect: JSONSchema.make
}
```
Zod 3: `zodToJsonSchema` from `zod-to-json-schema`
## OpenAPI Type Gen
Generate docs from types:
```typescript
import { fromTypes } from '@elysiajs/openapi'
export const app = new Elysia()
.use(openapi({
references: fromTypes()
}))
```
### Production
Recommended to generate `.d.ts` file for production when using OpenAPI Type Gen
```typescript
references: fromTypes(
process.env.NODE_ENV === 'production'
? 'dist/index.d.ts'
: 'src/index.ts'
)
```
### Options
```typescript
fromTypes('src/index.ts', {
projectRoot: path.join('..', import.meta.dir),
tsconfigPath: 'tsconfig.dts.json'
})
```
### Caveat: Explicit Types
Use `Prettify` helper to inline when type is not showing:
```typescript
type Prettify<T> = { [K in keyof T]: T[K] } & {}
function getUser(): Prettify<User> { }
```
## Schema Description
```typescript
body: t.Object({
username: t.String(),
password: t.String({
minLength: 8,
description: 'Password (8+ chars)'
})
}, {
description: 'Expected username and password'
}),
detail: {
summary: 'Sign in user',
tags: ['auth']
}
```
## Response Headers
```typescript
import { withHeader } from '@elysiajs/openapi'
response: withHeader(
t.Literal('Hi'),
{ 'x-powered-by': t.Literal('Elysia') }
)
```
Annotation only - doesn't enforce. Set headers manually.
## Tags
Define + assign:
```typescript
.use(openapi({
documentation: {
tags: [
{ name: 'App', description: 'General' },
{ name: 'Auth', description: 'Auth' }
]
}
}))
.get('/', () => 'hello', {
detail: { tags: ['App'] }
})
```
### Instance Tags
```typescript
new Elysia({ tags: ['user'] })
.get('/user', 'user')
```
## Reference Models
Auto-generates schemas:
```typescript
.model({
User: t.Object({
id: t.Number(),
username: t.String()
})
})
.get('/user', () => ({ id: 1, username: 'x' }), {
response: { 200: 'User' },
detail: { tags: ['User'] }
})
```
## Guard
Apply to instance/group:
```typescript
.guard({
detail: {
description: 'Requires auth'
}
})
.get('/user', 'user')
```
## Security
```typescript
.use(openapi({
documentation: {
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT'
}
}
}
}
}))
new Elysia({
prefix: '/address',
detail: {
security: [{ bearerAuth: [] }]
}
})
```
Secures all routes under prefix.
## Config
Below is a config which is accepted by the `openapi({})`
### enabled
@default true
Enable/Disable the plugin
### documentation
OpenAPI documentation information
@see https://spec.openapis.org/oas/v3.0.3.html
### exclude
Configuration to exclude paths or methods from documentation
### exclude.methods
List of methods to exclude from documentation
### exclude.paths
List of paths to exclude from documentation
### exclude.staticFile
@default true
Exclude static file routes from documentation
### exclude.tags
List of tags to exclude from documentation
### mapJsonSchema
A custom mapping function from Standard schema to OpenAPI schema
### path
@default '/openapi'
The endpoint to expose OpenAPI documentation frontend
### provider
@default 'scalar'
OpenAPI documentation frontend between:
- Scalar
- SwaggerUI
- null: disable frontend

View File

@@ -0,0 +1,167 @@
# OpenTelemetry Plugin - SKILLS.md
## Installation
```bash
bun add @elysiajs/opentelemetry
```
## Basic Usage
```typescript
import { opentelemetry } from '@elysiajs/opentelemetry'
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-node'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'
new Elysia()
.use(opentelemetry({
spanProcessors: [
new BatchSpanProcessor(new OTLPTraceExporter())
]
}))
```
Auto-collects spans from OpenTelemetry-compatible libraries. Parent/child spans applied automatically.
## Config
Extends OpenTelemetry SDK params:
- `autoDetectResources` (true) - Auto-detect from env
- `contextManager` (AsyncHooksContextManager) - Custom context
- `textMapPropagator` (CompositePropagator) - W3C Trace + Baggage
- `metricReader` - For MeterProvider
- `views` - Histogram bucket config
- `instrumentations` (getNodeAutoInstrumentations()) - Metapackage or individual
- `resource` - Custom resource
- `resourceDetectors` ([envDetector, processDetector, hostDetector]) - Auto-detect needs `autoDetectResources: true`
- `sampler` - Custom sampler (default: sample all)
- `serviceName` - Namespace identifier
- `spanProcessors` - Array for tracer provider
- `traceExporter` - Auto-setup OTLP/http/protobuf with BatchSpanProcessor if not set
- `spanLimits` - Tracing params
### Resource Detectors via Env
```bash
export OTEL_NODE_RESOURCE_DETECTORS="env,host"
# Options: env, host, os, process, serviceinstance, all, none
```
## Export to Backends
Example - Axiom:
```typescript
.use(opentelemetry({
spanProcessors: [
new BatchSpanProcessor(
new OTLPTraceExporter({
url: 'https://api.axiom.co/v1/traces',
headers: {
Authorization: `Bearer ${Bun.env.AXIOM_TOKEN}`,
'X-Axiom-Dataset': Bun.env.AXIOM_DATASET
}
})
)
]
}))
```
## OpenTelemetry SDK
Use SDK normally - runs under Elysia's request span, auto-appears in trace.
## Record Utility
Equivalent to `startActiveSpan` - auto-closes + captures exceptions:
```typescript
import { record } from '@elysiajs/opentelemetry'
.get('', () => {
return record('database.query', () => {
return db.query('SELECT * FROM users')
})
})
```
Label for code shown in trace.
## Function Naming
Elysia reads function names as span names:
```typescript
// ⚠️ Anonymous span
.derive(async ({ cookie: { session } }) => {
return { user: await getProfile(session) }
})
// ✅ Named span: "getProfile"
.derive(async function getProfile({ cookie: { session } }) {
return { user: await getProfile(session) }
})
```
## getCurrentSpan
Get current span outside handler (via AsyncLocalStorage):
```typescript
import { getCurrentSpan } from '@elysiajs/opentelemetry'
function utility() {
const span = getCurrentSpan()
span.setAttributes({ 'custom.attribute': 'value' })
}
```
## setAttributes
Sugar for `getCurrentSpan().setAttributes`:
```typescript
import { setAttributes } from '@elysiajs/opentelemetry'
function utility() {
setAttributes({ 'custom.attribute': 'value' })
}
```
## Instrumentations (Advanced)
SDK must run before importing instrumented module.
### Setup
1. Separate file:
```typescript
// src/instrumentation.ts
import { opentelemetry } from '@elysiajs/opentelemetry'
import { PgInstrumentation } from '@opentelemetry/instrumentation-pg'
export const instrumentation = opentelemetry({
instrumentations: [new PgInstrumentation()]
})
```
2. Apply:
```typescript
// src/index.ts
import { instrumentation } from './instrumentation'
new Elysia().use(instrumentation).listen(3000)
```
3. Preload:
```toml
# bunfig.toml
preload = ["./src/instrumentation.ts"]
```
### Production Deployment (Advanced)
OpenTelemetry monkey-patches `node_modules`. Exclude instrumented libs from bundling:
```bash
bun build --compile --external pg --outfile server src/index.ts
```
Package.json:
```json
{
"dependencies": { "pg": "^8.15.6" },
"devDependencies": {
"@elysiajs/opentelemetry": "^1.2.0",
"@opentelemetry/instrumentation-pg": "^0.52.0"
}
}
```
Production install:
```bash
bun install --production
```
Keeps `node_modules` with instrumented libs at runtime.

View File

@@ -0,0 +1,71 @@
# Server Timing Plugin
This plugin adds support for auditing performance bottlenecks with Server Timing API
## Installation
```bash
bun add @elysiajs/server-timing
```
## Basic Usage
```typescript twoslash
import { Elysia } from 'elysia'
import { serverTiming } from '@elysiajs/server-timing'
new Elysia()
.use(serverTiming())
.get('/', () => 'hello')
.listen(3000)
```
Server Timing then will append header 'Server-Timing' with log duration, function name, and detail for each life-cycle function.
To inspect, open browser developer tools > Network > [Request made through Elysia server] > Timing.
Now you can effortlessly audit the performance bottleneck of your server.
## Config
Below is a config which is accepted by the plugin
### enabled
@default `NODE_ENV !== 'production'`
Determine whether or not Server Timing should be enabled
### allow
@default `undefined`
A condition whether server timing should be log
### trace
@default `undefined`
Allow Server Timing to log specified life-cycle events:
Trace accepts objects of the following:
- request: capture duration from request
- parse: capture duration from parse
- transform: capture duration from transform
- beforeHandle: capture duration from beforeHandle
- handle: capture duration from the handle
- afterHandle: capture duration from afterHandle
- total: capture total duration from start to finish
## Pattern
Below you can find the common patterns to use the plugin.
## Allow Condition
You may disable Server Timing on specific routes via `allow` property
```ts twoslash
import { Elysia } from 'elysia'
import { serverTiming } from '@elysiajs/server-timing'
new Elysia()
.use(
serverTiming({
allow: ({ request }) => {
return new URL(request.url).pathname !== '/no-trace'
}
})
)
```

View File

@@ -0,0 +1,84 @@
# Static Plugin
This plugin can serve static files/folders for Elysia Server
## Installation
```bash
bun add @elysiajs/static
```
## Basic Usage
```typescript twoslash
import { Elysia } from 'elysia'
import { staticPlugin } from '@elysiajs/static'
new Elysia()
.use(staticPlugin())
.listen(3000)
```
By default, the static plugin default folder is `public`, and registered with `/public` prefix.
Suppose your project structure is:
```
| - src
| - index.ts
| - public
| - takodachi.png
| - nested
| - takodachi.png
```
The available path will become:
- /public/takodachi.png
- /public/nested/takodachi.png
## Config
Below is a config which is accepted by the plugin
### assets
@default `"public"`
Path to the folder to expose as static
### prefix
@default `"/public"`
Path prefix to register public files
### ignorePatterns
@default `[]`
List of files to ignore from serving as static files
### staticLimit
@default `1024`
By default, the static plugin will register paths to the Router with a static name, if the limits are exceeded, paths will be lazily added to the Router to reduce memory usage.
Tradeoff memory with performance.
### alwaysStatic
@default `false`
If set to true, static files path will be registered to Router skipping the `staticLimits`.
### headers
@default `{}`
Set response headers of files
### indexHTML
@default `false`
If set to true, the `index.html` file from the static directory will be served for any request that is matching neither a route nor any existing static file.
## Pattern
Below you can find the common patterns to use the plugin.
## Single file
Suppose you want to return just a single file, you can use `file` instead of using the static plugin
```typescript
import { Elysia, file } from 'elysia'
new Elysia()
.get('/file', file('public/takodachi.png'))
```

View File

@@ -0,0 +1,129 @@
# Fullstack Dev Server
## What It Is
Bun 1.3 Fullstack Dev Server with HMR. React without bundler (no Vite/Webpack).
Example: [elysia-fullstack-example](https://github.com/saltyaom/elysia-fullstack-example)
## Setup
1. Install + use Elysia Static:
```typescript
import { Elysia } from 'elysia'
import { staticPlugin } from '@elysiajs/static'
new Elysia()
.use(await staticPlugin()) // await required for HMR hooks
.listen(3000)
```
2. Create `public/index.html` + `public/index.tsx`:
```html
<!-- public/index.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Elysia React App</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div id="root"></div>
<script type="module" src="./index.tsx"></script>
</body>
</html>
```
```tsx
// public/index.tsx
import { useState } from 'react'
import { createRoot } from 'react-dom/client'
function App() {
const [count, setCount] = useState(0)
const increase = () => setCount((c) => c + 1)
return (
<main>
<h2>{count}</h2>
<button onClick={increase}>Increase</button>
</main>
)
}
const root = createRoot(document.getElementById('root')!)
root.render(<App />)
```
3. Enable JSX in `tsconfig.json`:
```json
{
"compilerOptions": {
"jsx": "react-jsx"
}
}
```
4. Navigate to `http://localhost:3000/public`.
Frontend + backend in single project. No bundler. Works with HMR, Tailwind, Tanstack Query, Eden Treaty, path alias.
## Custom Prefix
```typescript
.use(await staticPlugin({ prefix: '/' }))
```
Serves at `/` instead of `/public`.
## Tailwind CSS
1. Install:
```bash
bun add tailwindcss@4
bun add -d bun-plugin-tailwind
```
2. Create `bunfig.toml`:
```toml
[serve.static]
plugins = ["bun-plugin-tailwind"]
```
3. Create `public/global.css`:
```css
@tailwind base;
```
4. Add to HTML or TS:
```html
<link rel="stylesheet" href="tailwindcss">
```
Or:
```tsx
import './global.css'
```
## Path Alias
1. Add to `tsconfig.json`:
```json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@public/*": ["public/*"]
}
}
}
```
2. Use:
```tsx
import '@public/global.css'
```
Works out of box.
## Production Build
```bash
bun build --compile --target bun --outfile server src/index.ts
```
Creates single executable `server`. Include `public` folder when running.

View File

@@ -0,0 +1,187 @@
# Cookie
## What It Is
Reactive mutable signal for cookie interaction. Auto-encodes/decodes objects.
## Basic Usage
No get/set - direct value access:
```typescript
import { Elysia } from 'elysia'
new Elysia()
.get('/', ({ cookie: { name } }) => {
// Get
name.value
// Set
name.value = "New Value"
})
```
Auto-encodes/decodes objects. Just works.
## Reactivity
Signal-like approach. Single source of truth. Auto-sets headers, syncs values.
Cookie jar = Proxy object. Extract value always `Cookie<unknown>`, never `undefined`. Access via `.value`.
Iterate over cookie jar → only existing cookies.
## Cookie Attributes
### Direct Property Assignment
```typescript
.get('/', ({ cookie: { name } }) => {
// Get
name.domain
// Set
name.domain = 'millennium.sh'
name.httpOnly = true
})
```
### set - Reset All Properties
```typescript
.get('/', ({ cookie: { name } }) => {
name.set({
domain: 'millennium.sh',
httpOnly: true
})
})
```
Overwrites all properties.
### add - Update Specific Properties
Like `set` but only overwrites defined properties.
## Remove Cookie
```typescript
.get('/', ({ cookie, cookie: { name } }) => {
name.remove()
// or
delete cookie.name
})
```
## Cookie Schema
Strict validation + type inference with `t.Cookie`:
```typescript
import { Elysia, t } from 'elysia'
new Elysia()
.get('/', ({ cookie: { name } }) => {
name.value = {
id: 617,
name: 'Summoning 101'
}
}, {
cookie: t.Cookie({
name: t.Object({
id: t.Numeric(),
name: t.String()
})
})
})
```
### Nullable Cookie
```typescript
cookie: t.Cookie({
name: t.Optional(
t.Object({
id: t.Numeric(),
name: t.String()
})
)
})
```
## Cookie Signature
Cryptographic hash for verification. Prevents malicious modification.
```typescript
new Elysia()
.get('/', ({ cookie: { profile } }) => {
profile.value = { id: 617, name: 'Summoning 101' }
}, {
cookie: t.Cookie({
profile: t.Object({
id: t.Numeric(),
name: t.String()
})
}, {
secrets: 'Fischl von Luftschloss Narfidort',
sign: ['profile']
})
})
```
Auto-signs/unsigns.
### Global Config
```typescript
new Elysia({
cookie: {
secrets: 'Fischl von Luftschloss Narfidort',
sign: ['profile']
}
})
```
## Cookie Rotation
Auto-handles secret rotation. Old signature verification + new signature signing.
```typescript
new Elysia({
cookie: {
secrets: ['Vengeance will be mine', 'Fischl von Luftschloss Narfidort']
}
})
```
Array = key rotation (retire old, replace with new).
## Config
### secrets
Secret key for signing/unsigning. Array = key rotation.
### domain
Domain Set-Cookie attribute. Default: none (current domain only).
### encode
Function to encode value. Default: `encodeURIComponent`.
### expires
Date for Expires attribute. Default: none (non-persistent, deleted on browser exit).
If both `expires` and `maxAge` set, `maxAge` takes precedence (spec-compliant clients).
### httpOnly (false)
HttpOnly attribute. If true, JS can't access via `document.cookie`.
### maxAge (undefined)
Seconds for Max-Age attribute. Rounded down to integer.
If both `expires` and `maxAge` set, `maxAge` takes precedence (spec-compliant clients).
### path
Path attribute. Default: handler path.
### priority
Priority attribute: `low` | `medium` | `high`. Not fully standardized.
### sameSite
SameSite attribute:
- `true` = Strict
- `false` = not set
- `'lax'` = Lax
- `'none'` = None (explicit cross-site)
- `'strict'` = Strict
Not fully standardized.
### secure
Secure attribute. If true, only HTTPS. Clients won't send over HTTP.

View File

@@ -0,0 +1,413 @@
# Deployment
## Production Build
### Compile to Binary (Recommended)
```bash
bun build \
--compile \
--minify-whitespace \
--minify-syntax \
--target bun \
--outfile server \
src/index.ts
```
**Benefits:**
- No runtime needed on deployment server
- Smaller memory footprint (2-3x reduction)
- Faster startup
- Single portable executable
**Run the binary:**
```bash
./server
```
### Compile to JavaScript
```bash
bun build \
--minify-whitespace \
--minify-syntax \
--outfile ./dist/index.js \
src/index.ts
```
**Run:**
```bash
NODE_ENV=production bun ./dist/index.js
```
## Docker
### Basic Dockerfile
```dockerfile
FROM oven/bun:1 AS build
WORKDIR /app
# Cache dependencies
COPY package.json bun.lock ./
RUN bun install
COPY ./src ./src
ENV NODE_ENV=production
RUN bun build \
--compile \
--minify-whitespace \
--minify-syntax \
--outfile server \
src/index.ts
FROM gcr.io/distroless/base
WORKDIR /app
COPY --from=build /app/server server
ENV NODE_ENV=production
CMD ["./server"]
EXPOSE 3000
```
### Build and Run
```bash
docker build -t my-elysia-app .
docker run -p 3000:3000 my-elysia-app
```
### With Environment Variables
```dockerfile
FROM gcr.io/distroless/base
WORKDIR /app
COPY --from=build /app/server server
ENV NODE_ENV=production
ENV PORT=3000
ENV DATABASE_URL=""
ENV JWT_SECRET=""
CMD ["./server"]
EXPOSE 3000
```
## Cluster Mode (Multiple CPU Cores)
```typescript
// src/index.ts
import cluster from 'node:cluster'
import os from 'node:os'
import process from 'node:process'
if (cluster.isPrimary) {
for (let i = 0; i < os.availableParallelism(); i++) {
cluster.fork()
}
} else {
await import('./server')
console.log(`Worker ${process.pid} started`)
}
```
```typescript
// src/server.ts
import { Elysia } from 'elysia'
new Elysia()
.get('/', () => 'Hello World!')
.listen(3000)
```
## Environment Variables
### .env File
```env
NODE_ENV=production
PORT=3000
DATABASE_URL=postgresql://user:password@localhost:5432/db
JWT_SECRET=your-secret-key
CORS_ORIGIN=https://example.com
```
### Load in App
```typescript
import { Elysia } from 'elysia'
const app = new Elysia()
.get('/env', () => ({
env: process.env.NODE_ENV,
port: process.env.PORT
}))
.listen(parseInt(process.env.PORT || '3000'))
```
## Platform-Specific Deployments
### Railway
```typescript
// Railway assigns random PORT via env variable
new Elysia()
.get('/', () => 'Hello Railway')
.listen(process.env.PORT ?? 3000)
```
### Vercel
```typescript
// src/index.ts
import { Elysia } from 'elysia'
export default new Elysia()
.get('/', () => 'Hello Vercel')
export const GET = app.fetch
export const POST = app.fetch
```
```json
// vercel.json
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"bunVersion": "1.x"
}
```
### Cloudflare Workers
```typescript
import { Elysia } from 'elysia'
import { CloudflareAdapter } from 'elysia/adapter/cloudflare-worker'
export default new Elysia({
adapter: CloudflareAdapter
})
.get('/', () => 'Hello Cloudflare!')
.compile()
```
```toml
# wrangler.toml
name = "elysia-app"
main = "src/index.ts"
compatibility_date = "2025-06-01"
```
### Node.js Adapter
```typescript
import { Elysia } from 'elysia'
import { node } from '@elysiajs/node'
const app = new Elysia({ adapter: node() })
.get('/', () => 'Hello Node.js')
.listen(3000)
```
## Performance Optimization
### Enable AoT Compilation
```typescript
new Elysia({
aot: true // Ahead-of-time compilation
})
```
### Use Native Static Response
```typescript
new Elysia({
nativeStaticResponse: true
})
.get('/version', 1) // Optimized for Bun.serve.static
```
### Precompile Routes
```typescript
new Elysia({
precompile: true // Compile all routes ahead of time
})
```
## Health Checks
```typescript
new Elysia()
.get('/health', () => ({
status: 'ok',
timestamp: Date.now()
}))
.get('/ready', ({ db }) => {
// Check database connection
const isDbReady = checkDbConnection()
if (!isDbReady) {
return status(503, { status: 'not ready' })
}
return { status: 'ready' }
})
```
## Graceful Shutdown
```typescript
import { Elysia } from 'elysia'
const app = new Elysia()
.get('/', () => 'Hello')
.listen(3000)
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down gracefully')
app.stop()
process.exit(0)
})
process.on('SIGINT', () => {
console.log('SIGINT received, shutting down gracefully')
app.stop()
process.exit(0)
})
```
## Monitoring
### OpenTelemetry
```typescript
import { opentelemetry } from '@elysiajs/opentelemetry'
new Elysia()
.use(opentelemetry({
serviceName: 'my-service',
endpoint: 'http://localhost:4318'
}))
```
### Custom Logging
```typescript
.onRequest(({ request }) => {
console.log(`[${new Date().toISOString()}] ${request.method} ${request.url}`)
})
.onAfterResponse(({ request, set }) => {
console.log(`[${new Date().toISOString()}] ${request.method} ${request.url} - ${set.status}`)
})
```
## SSL/TLS (HTTPS)
```typescript
import { Elysia, file } from 'elysia'
new Elysia({
serve: {
tls: {
cert: file('cert.pem'),
key: file('key.pem')
}
}
})
.get('/', () => 'Hello HTTPS')
.listen(3000)
```
## Best Practices
1. **Always compile to binary for production**
- Reduces memory usage
- Smaller deployment size
- No runtime needed
2. **Use environment variables**
- Never hardcode secrets
- Use different configs per environment
3. **Enable health checks**
- Essential for load balancers
- K8s/Docker orchestration
4. **Implement graceful shutdown**
- Handle SIGTERM/SIGINT
- Close connections properly
5. **Use cluster mode**
- Utilize all CPU cores
- Better performance under load
6. **Monitor your app**
- Use OpenTelemetry
- Log requests/responses
- Track errors
## Example Production Setup
```typescript
// src/server.ts
import { Elysia } from 'elysia'
import { cors } from '@elysiajs/cors'
import { opentelemetry } from '@elysiajs/opentelemetry'
export const app = new Elysia({
aot: true,
nativeStaticResponse: true
})
.use(cors({
origin: process.env.CORS_ORIGIN || 'http://localhost:3000'
}))
.use(opentelemetry({
serviceName: 'my-service'
}))
.get('/health', () => ({ status: 'ok' }))
.get('/', () => 'Hello Production')
.listen(parseInt(process.env.PORT || '3000'))
// Graceful shutdown
process.on('SIGTERM', () => {
app.stop()
process.exit(0)
})
```
```typescript
// src/index.ts (cluster)
import cluster from 'node:cluster'
import os from 'node:os'
if (cluster.isPrimary) {
for (let i = 0; i < os.availableParallelism(); i++) {
cluster.fork()
}
} else {
await import('./server')
}
```
```dockerfile
# Dockerfile
FROM oven/bun:1 AS build
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install
COPY ./src ./src
ENV NODE_ENV=production
RUN bun build --compile --outfile server src/index.ts
FROM gcr.io/distroless/base
WORKDIR /app
COPY --from=build /app/server server
ENV NODE_ENV=production
CMD ["./server"]
EXPOSE 3000
```

View File

@@ -0,0 +1,158 @@
# Eden Treaty
e2e type safe RPC client for share type from backend to frontend.
## What It Is
Type-safe object representation for Elysia server. Auto-completion + error handling.
## Installation
```bash
bun add @elysiajs/eden
bun add -d elysia
```
Export Elysia server type:
```typescript
const app = new Elysia()
.get('/', () => 'Hi Elysia')
.get('/id/:id', ({ params: { id } }) => id)
.post('/mirror', ({ body }) => body, {
body: t.Object({
id: t.Number(),
name: t.String()
})
})
.listen(3000)
export type App = typeof app
```
Consume on client side:
```typescript
import { treaty } from '@elysiajs/eden'
import type { App } from './server'
const client = treaty<App>('localhost:3000')
// response: Hi Elysia
const { data: index } = await client.get()
// response: 1895
const { data: id } = await client.id({ id: 1895 }).get()
// response: { id: 1895, name: 'Skadi' }
const { data: nendoroid } = await client.mirror.post({
id: 1895,
name: 'Skadi'
})
```
## Common Errors & Fixes
- **Strict mode**: Enable in tsconfig
- **Version mismatch**: `npm why elysia` - must match server/client
- **TypeScript**: Min 5.0
- **Method chaining**: Required on server
- **Bun types**: `bun add -d @types/bun` if using Bun APIs
- **Path alias**: Must resolve same on frontend/backend
### Monorepo Path Alias
Must resolve to same file on frontend/backend
```json
// tsconfig.json at root
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@frontend/*": ["./apps/frontend/src/*"],
"@backend/*": ["./apps/backend/src/*"]
}
}
}
```
## Syntax Mapping
| Path | Method | Treaty |
|----------------|--------|-------------------------------|
| / | GET | `.get()` |
| /hi | GET | `.hi.get()` |
| /deep/nested | POST | `.deep.nested.post()` |
| /item/:name | GET | `.item({ name: 'x' }).get()` |
## Parameters
### With body (POST/PUT/PATCH/DELETE):
```typescript
.user.post(
{ name: 'Elysia' }, // body
{ headers: {}, query: {}, fetch: {} } // optional
)
```
### No body (GET/HEAD):
```typescript
.hello.get({ headers: {}, query: {}, fetch: {} })
```
### Empty body with query/headers:
```typescript
.user.post(null, { query: { name: 'Ely' } })
```
### Fetch options:
```typescript
.hello.get({ fetch: { signal: controller.signal } })
```
### File upload:
```typescript
// Accepts: File | File[] | FileList | Blob
.image.post({
title: 'Title',
image: fileInput.files!
})
```
## Response
```typescript
const { data, error, response, status, headers } = await api.user.post({ name: 'x' })
if (error) {
switch (error.status) {
case 400: throw error.value
default: throw error.value
}
}
// data unwrapped after error handling
return data
```
status >= 300 → `data = null`, `error` has value
## Stream/SSE
Interpreted as `AsyncGenerator`:
```typescript
const { data, error } = await treaty(app).ok.get()
if (error) throw error
for await (const chunk of data) console.log(chunk)
```
## Utility Types
```typescript
import { Treaty } from '@elysiajs/eden'
type UserData = Treaty.Data<typeof api.user.post>
type UserError = Treaty.Error<typeof api.user.post>
```
## WebSocket
```typescript
const chat = api.chat.subscribe()
chat.subscribe((message) => console.log('got', message))
chat.on('open', () => chat.send('hello'))
// Native access: chat.raw
```
`.subscribe()` accepts same params as `get`/`head`

View File

@@ -0,0 +1,198 @@
# Lifecycle
Instead of a sequential process, Elysia's request handling is divided into multiple stages called lifecycle events.
It's designed to separate the process into distinct phases based on their responsibility without interfering with each others.
### List of events in order
1. **request** - early, global
2. **parse** - body parsing
3. **transform** / **derive** - mutate context pre validation
4. **beforeHandle** / **resolve** - auth/guard logic
5. **handler** - your business code
6. **afterHandle** - tweak response, set headers
7. **mapResponse** - turn anything into a proper `Response`
8. **onError** - centralized error handling
9. **onAfterResponse** - post response/cleanup tasks
## Request (`onRequest`)
Runs first for every incoming request.
- Ideal for **caching, rate limiting, CORS, adding global headers**.
- If the hook returns a value, the whole lifecycle stops and that value becomes the response.
```ts
new Elysia().onRequest(({ ip, set }) => {
if (blocked(ip)) return (set.status = 429)
})
```
---
## Parse (`onParse`)
_Body parsing stage._
- Handles `text/plain`, `application/json`, `multipart/form-data`, `application/x www-form-urlencoded` by default.
- Use to add **custom parsers** or support extra `Content Type`s.
```ts
new Elysia().onParse(({ request, contentType }) => {
if (contentType === 'application/custom') return request.text()
})
```
---
## Transform (`onTransform`)
_Runs **just before validation**; can mutate the request context._
- Perfect for **type coercion**, trimming strings, or adding temporary fields that validation will use.
```ts
new Elysia().onTransform(({ params }) => {
params.id = Number(params.id)
})
```
---
## Derive
_Runs along with `onTransform` **but before validation**; adds per request values to the context._
- Useful for extracting info from headers, cookies, query, etc., that you want to reuse in handlers.
```ts
new Elysia().derive(({ headers }) => ({
bearer: headers.authorization?.replace(/^Bearer /, '')
}))
```
---
## Before Handle (`onBeforeHandle`)
_Executed after validation, right before the route handler._
- Great for **auth checks, permission gating, custom pre validation logic**.
- Returning a value skips the handler.
```ts
new Elysia().get('/', () => 'hi', {
beforeHandle({ cookie, status }) {
if (!cookie.session) return status(401)
}
})
```
---
## Resolve
_Like `derive` but runs **after validation** along "Before Handle" (so you can rely on validated data)._
- Usually placed inside a `guard` because it isn't available as a local hook.
```ts
new Elysia().guard(
{ headers: t.Object({ authorization: t.String() }) },
(app) =>
app
.resolve(({ headers }) => ({
bearer: headers.authorization.split(' ')[1]
}))
.get('/', ({ bearer }) => bearer)
)
```
---
## After Handle (`onAfterHandle`)
_Runs after the handler finishes._
- Can **modify response headers**, wrap the result in a `Response`, or transform the payload.
- Returning a value **replaces** the handler’s result, but the next `afterHandle` hooks still run.
```ts
new Elysia().get('/', () => '<h1>Hello</h1>', {
afterHandle({ response, set }) {
if (isHtml(response)) {
set.headers['content-type'] = 'text/html; charset=utf-8'
return new Response(response)
}
}
})
```
---
## Map Response (`mapResponse`)
_Runs right after all `afterHandle` hooks; maps **any** value to a Web standard `Response`._
- Ideal for **compression, custom content type mapping, streaming**.
```ts
new Elysia().mapResponse(({ responseValue, set }) => {
const body =
typeof responseValue === 'object'
? JSON.stringify(responseValue)
: String(responseValue ?? '')
set.headers['content-encoding'] = 'gzip'
return new Response(Bun.gzipSync(new TextEncoder().encode(body)), {
headers: {
'Content-Type':
typeof responseValue === 'object'
? 'application/json'
: 'text/plain'
}
})
})
```
---
## On Error (`onError`)
_Caught whenever an error bubbles up from any lifecycle stage._
- Use to **customize error messages**, **handle 404**, **log**, or **retry**.
- Must be registered **before** the routes it should protect.
```ts
new Elysia().onError(({ code, status }) => {
if (code === 'NOT_FOUND') return status(404, '❓ Not found')
return new Response('Oops', { status: 500 })
})
```
---
## After Response (`onAfterResponse`)
_Runs **after** the response has been sent to the client._
- Perfect for **logging, metrics, cleanup**.
```ts
new Elysia().onAfterResponse(() =>
console.log('✅ response sent at', Date.now())
)
```
---
## Hook Types
| Type | Scope | How to add |
| -------------------- | --------------------------------- | --------------------------------------------------------- |
| **Local Hook** | Single route | Inside route options (`afterHandle`, `beforeHandle`, …) |
| **Interceptor Hook** | Whole instance (and later routes) | `.onXxx(cb)` or `.use(plugin)` |
> **Remember:** Hooks only affect routes **defined after** they are registered, except `onRequest` which is global because it runs before route matching.

View File

@@ -0,0 +1,83 @@
# Macro
Composable Elysia function for controlling lifecycle/schema/context with full type safety. Available in hook after definition control by key-value label.
## Basic Pattern
```typescript
.macro({
hi: (word: string) => ({
beforeHandle() { console.log(word) }
})
})
.get('/', () => 'hi', { hi: 'Elysia' })
```
## Property Shorthand
Object → function accepting boolean:
```typescript
.macro({
// These equivalent:
isAuth: { resolve: () => ({ user: 'saltyaom' }) },
isAuth(enabled: boolean) { if(enabled) return { resolve() {...} } }
})
```
## Error Handling
Return `status`, don't throw:
```typescript
.macro({
auth: {
resolve({ headers }) {
if(!headers.authorization) return status(401, 'Unauthorized')
return { user: 'SaltyAom' }
}
}
})
```
## Resolve - Add Context Props
```typescript
.macro({
user: (enabled: true) => ({
resolve: () => ({ user: 'Pardofelis' })
})
})
.get('/', ({ user }) => user, { user: true })
```
### Named Macro for Type Inference
TypeScript limitation workaround:
```typescript
.macro('user', { resolve: () => ({ user: 'lilith' }) })
.macro('user2', { user: true, resolve: ({ user }) => {} })
```
## Schema
Auto-validates, infers types, stacks with other schemas:
```typescript
.macro({
withFriends: {
body: t.Object({ friends: t.Tuple([...]) })
}
})
```
Use named single macro for lifecycle type inference within same macro.
## Extension
Stack macros:
```typescript
.macro({
sartre: { body: t.Object({...}) },
fouco: { body: t.Object({...}) },
lilith: { fouco: true, sartre: true, body: t.Object({...}) }
})
```
## Deduplication
Auto-dedupes by property value. Custom seed:
```typescript
.macro({ sartre: (role: string) => ({ seed: role, ... }) })
```
Max stack: 16 (prevents infinite loops)

View File

@@ -0,0 +1,207 @@
# Plugins
## Plugin = Decoupled Elysia Instance
```ts
const plugin = new Elysia()
.decorate('plugin', 'hi')
.get('/plugin', ({ plugin }) => plugin)
const app = new Elysia()
.use(plugin) // inherit properties
.get('/', ({ plugin }) => plugin)
```
**Inherits**: state, decorate
**Does NOT inherit**: lifecycle (isolated by default)
## Dependency
Each instance runs independently like microservice. **Must explicitly declare dependencies**.
```ts
const auth = new Elysia()
.decorate('Auth', Auth)
// ❌ Missing dependency
const main = new Elysia()
.get('/', ({ Auth }) => Auth.getProfile())
// ✅ Declare dependency
const main = new Elysia()
.use(auth) // required for Auth
.get('/', ({ Auth }) => Auth.getProfile())
```
## Deduplication
**Every plugin re-executes by default**. Use `name` + optional `seed` to deduplicate:
```ts
const ip = new Elysia({ name: 'ip' }) // unique identifier
.derive({ as: 'global' }, ({ server, request }) => ({
ip: server?.requestIP(request)
}))
const router1 = new Elysia().use(ip)
const router2 = new Elysia().use(ip)
const server = new Elysia().use(router1).use(router2)
// `ip` only executes once due to deduplication
```
## Global vs Explicit Dependency
**Global plugin** (rare, apply everywhere):
- Doesn't add types - cors, compress, helmet
- Global lifecycle no instance controls - tracing, logging
- Examples: OpenAPI docs, OpenTelemetry, logging
**Explicit dependency** (default, recommended):
- Adds types - macro, state, model
- Business logic instances interact with - Auth, DB
- Examples: state management, ORM, auth, features
## Scope
**Lifecycle isolated by default**. Must specify scope to export.
```ts
// ❌ NOT inherited by app
const profile = new Elysia()
.onBeforeHandle(({ cookie }) => throwIfNotSignIn(cookie))
.get('/profile', () => 'Hi')
const app = new Elysia()
.use(profile)
.patch('/rename', ({ body }) => updateProfile(body)) // No sign-in check
// ✅ Exported to app
const profile = new Elysia()
.onBeforeHandle({ as: 'global' }, ({ cookie }) => throwIfNotSignIn(cookie))
.get('/profile', () => 'Hi')
```
## Scope Levels
1. **local** (default) - current + descendants only
2. **scoped** - parent + current + descendants
3. **global** - all instances (all parents, current, descendants)
Example with `.onBeforeHandle({ as: 'local' }, ...)`:
| type | child | current | parent | main |
|------|-------|---------|--------|------|
| local | ✅ | ✅ | ❌ | ❌ |
| scoped | ✅ | ✅ | ✅ | ❌ |
| global | ✅ | ✅ | ✅ | ✅ |
## Config
```ts
// Instance factory with config
const version = (v = 1) => new Elysia()
.get('/version', v)
const app = new Elysia()
.use(version(1))
```
## Functional Callback (not recommended)
```ts
// Harder to handle scope/encapsulation
const plugin = (app: Elysia) => app
.state('counter', 0)
.get('/plugin', () => 'Hi')
// Prefer new instance (better type inference, no perf diff)
```
## Guard (Apply to Multiple Routes)
```ts
.guard(
{ body: t.Object({ username: t.String(), password: t.String() }) },
(app) =>
app.post('/sign-up', ({ body }) => signUp(body))
.post('/sign-in', ({ body }) => signIn(body))
)
```
**Grouped guard** (merge group + guard):
```ts
.group(
'/v1',
{ body: t.Literal('Rikuhachima Aru') }, // guard here
(app) => app.post('/student', ({ body }) => body)
)
```
## Scope Casting
**3 methods to apply hook to parent**:
1. **Inline as** (single hook):
```ts
.derive({ as: 'scoped' }, () => ({ hi: 'ok' }))
```
2. **Guard as** (multiple hooks, no derive/resolve):
```ts
.guard({
as: 'scoped',
response: t.String(),
beforeHandle() { console.log('ok') }
})
```
3. **Instance as** (all hooks + schema):
```ts
const plugin = new Elysia()
.derive(() => ({ hi: 'ok' }))
.get('/child', ({ hi }) => hi)
.as('scoped') // lift scope up
```
`.as()` lifts scope: local → scoped → global
## Lazy Load
**Deferred module** (async plugin, non-blocking startup):
```ts
// plugin.ts
export const loadStatic = async (app: Elysia) => {
const files = await loadAllFiles()
files.forEach((asset) => app.get(asset, file(asset)))
return app
}
// main.ts
const app = new Elysia().use(loadStatic)
```
**Lazy-load module** (dynamic import):
```ts
const app = new Elysia()
.use(import('./plugin')) // loaded after startup
```
**Testing** (wait for modules):
```ts
await app.modules // ensure all deferred/lazy modules loaded
```
## Notes
[Inference] Based on docs patterns:
- Use inline values for static resources (performance optimization)
- Group routes by prefix for organization
- Extend context minimally (separation of concerns)
- Use `status()` over `set.status` for type safety
- Prefer `resolve()` over `derive()` when type integrity matters
- Plugins isolated by default (must declare scope explicitly)
- Use `name` for deduplication when plugin used multiple times
- Prefer explicit dependency over global (better modularity/tracking)

View File

@@ -0,0 +1,331 @@
# ElysiaJS: Routing, Handlers & Context
## Routing
### Path Types
```ts
new Elysia()
.get('/static', 'static path') // exact match
.get('/id/:id', 'dynamic path') // captures segment
.get('/id/*', 'wildcard path') // captures rest
```
**Path Priority**: static > dynamic > wildcard
### Dynamic Paths
```ts
new Elysia()
.get('/id/:id', ({ params: { id } }) => id)
.get('/id/:id/:name', ({ params: { id, name } }) => id + ' ' + name)
```
**Optional params**: `.get('/id/:id?', ...)`
### HTTP Verbs
- `.get()` - retrieve data
- `.post()` - submit/create
- `.put()` - replace
- `.patch()` - partial update
- `.delete()` - remove
- `.all()` - any method
- `.route(method, path, handler)` - custom verb
### Grouping Routes
```ts
new Elysia()
.group('/user', { body: t.Literal('auth') }, (app) =>
app.post('/sign-in', ...)
.post('/sign-up', ...)
)
// Or use prefix in constructor
new Elysia({ prefix: '/user' })
.post('/sign-in', ...)
```
## Handlers
### Handler = function accepting HTTP request, returning response
```ts
// Inline value (compiled ahead, optimized)
.get('/', 'Hello Elysia')
.get('/video', file('video.mp4'))
// Function handler
.get('/', () => 'hello')
.get('/', ({ params, query, body }) => {...})
```
### Context Properties
- `body` - HTTP message/form/file
- `query` - query string as object
- `params` - path parameters
- `headers` - HTTP headers
- `cookie` - mutable signal for cookies
- `store` - global mutable state
- `request` - Web Standard Request
- `server` - Bun server instance
- `path` - request pathname
### Context Utilities
```ts
import { redirect, form } from 'elysia'
new Elysia().get('/', ({ status, set, form }) => {
// Status code (type-safe)
status(418, "I'm a teapot")
// Set response props
set.headers['x-custom'] = 'value'
set.status = 418 // legacy, no type inference
// Redirect
return redirect('https://...', 302)
// Cookies (mutable signal, no get/set)
cookie.name.value // get
cookie.name.value = 'new' // set
// FormData response
return form({ name: 'Party', images: [file('a.jpg')] })
// Single file
return file('document.pdf')
})
```
### Streaming
```ts
new Elysia()
.get('/stream', function* () {
yield 1
yield 2
yield 3
})
// Server-Sent Events
.get('/sse', function* () {
yield sse('hello')
yield sse({ event: 'msg', data: {...} })
})
```
**Note**: Headers only settable before first yield
**Conditional stream**: returning without yield converts to normal response
## Context Extension
[Inference] Extend when property is:
- Global mutable (use `state`)
- Request/response related (use `decorate`)
- Derived from existing props (use `derive`/`resolve`)
### state() - Global Mutable
```ts
new Elysia()
`.state('version', 1)
.get('/', ({ store: { version } }) => version)
// Multiple
.state({ counter: 0, visits: 0 })
// Remap (create new from existing)
.state(({ version, ...store }) => ({
...store,
apiVersion: version
}))
````
**Gotcha**: Use reference not value
```ts
new Elysia()
// ✅ Correct
.get('/', ({ store }) => store.counter++)
// ❌ Wrong - loses reference
.get('/', ({ store: { counter } }) => counter++)
```
### decorate() - Additional Context Props
```ts
new Elysia()
.decorate('logger', new Logger())
.get('/', ({ logger }) => logger.log('hi'))
// Multiple
.decorate({ logger: new Logger(), db: connection })
```
**When**: constant/readonly values, classes with internal state, singletons
### derive() - Create from Existing (Transform Lifecycle)
```ts
new Elysia()
.derive(({ headers }) => ({
bearer: headers.authorization?.startsWith('Bearer ')
? headers.authorization.slice(7)
: null
}))
.get('/', ({ bearer }) => bearer)
```
**Timing**: runs at transform (before validation)
**Type safety**: request props typed as `unknown`
### resolve() - Type-Safe Derive (beforeHandle Lifecycle)
```ts
new Elysia()
.guard({
headers: t.Object({
bearer: t.String({ pattern: '^Bearer .+$' })
})
})
.resolve(({ headers }) => ({
bearer: headers.bearer.slice(7) // typed correctly
}))
```
**Timing**: runs at beforeHandle (after validation)
**Type safety**: request props fully typed
### Error from derive/resolve
```ts
new Elysia()
.derive(({ headers, status }) => {
if (!headers.authorization) return status(400)
return { bearer: ... }
})
```
Returns early if error returned
## Patterns
### Affix (Bulk Remap)
```ts
const plugin = new Elysia({ name: 'setup' }).decorate({
argon: 'a',
boron: 'b'
})
new Elysia()
.use(plugin)
.prefix('decorator', 'setup') // setupArgon, setupBoron
.prefix('all', 'setup') // remap everything
```
### Assignment Patterns
1. **key-value**: `.state('key', value)`
2. **object**: `.state({ k1: v1, k2: v2 })`
3. **remap**: `.state(({old}) => ({new}))`
## Testing
```ts
const app = new Elysia().get('/', 'hi')
// Programmatic test
app.handle(new Request('http://localhost/'))
```
## To Throw or Return
Most of an error handling in Elysia can be done by throwing an error and will be handle in `onError`.
But for `status` it can be a little bit confusing, since it can be used both as a return value or throw an error.
It could either be **return** or **throw** based on your specific needs.
- If an `status` is **throw**, it will be caught by `onError` middleware.
- If an `status` is **return**, it will be **NOT** caught by `onError` middleware.
See the following code:
```typescript
import { Elysia, file } from 'elysia'
new Elysia()
.onError(({ code, error, path }) => {
if (code === 418) return 'caught'
})
.get('/throw', ({ status }) => {
// This will be caught by onError
throw status(418)
})
.get('/return', ({ status }) => {
// This will NOT be caught by onError
return status(418)
})
```
## To Throw or Return
Elysia provide a `status` function for returning HTTP status code, prefers over `set.status`.
`status` can be import from Elysia but preferably extract from route handler Context for type safety.
```ts
import { Elysia, status } from 'elysia'
function doThing() {
if (Math.random() > 0.33) return status(418, "I'm a teapot")
}
new Elysia().get('/', ({ status }) => {
if (Math.random() > 0.33) return status(418)
return 'ok'
})
```
Error Handling in Elysia can be done by throwing an error and will be handle in `onError`.
Status could either be **return** or **throw** based on your specific needs.
- If an `status` is **throw**, it will be caught by `onError` middleware.
- If an `status` is **return**, it will be **NOT** caught by `onError` middleware.
See the following code:
```typescript
import { Elysia, file } from 'elysia'
new Elysia()
.onError(({ code, error, path }) => {
if (code === 418) return 'caught'
})
.get('/throw', ({ status }) => {
// This will be caught by onError
throw status(418)
})
.get('/return', ({ status }) => {
// This will NOT be caught by onError
return status(418)
})
```
## Notes
[Inference] Based on docs patterns:
- Use inline values for static resources (performance optimization)
- Group routes by prefix for organization
- Extend context minimally (separation of concerns)
- Use `status()` over `set.status` for type safety
- Prefer `resolve()` over `derive()` when type integrity matters

View File

@@ -0,0 +1,385 @@
# Unit Testing
## Basic Test Setup
### Installation
```bash
bun add -d @elysiajs/eden
```
### Basic Test
```typescript
// test/app.test.ts
import { describe, expect, it } from 'bun:test'
import { Elysia } from 'elysia'
describe('Elysia App', () => {
it('should return hello world', async () => {
const app = new Elysia()
.get('/', () => 'Hello World')
const res = await app.handle(
new Request('http://localhost/')
)
expect(res.status).toBe(200)
expect(await res.text()).toBe('Hello World')
})
})
```
## Testing Routes
### GET Request
```typescript
it('should get user by id', async () => {
const app = new Elysia()
.get('/user/:id', ({ params: { id } }) => ({
id,
name: 'John Doe'
}))
const res = await app.handle(
new Request('http://localhost/user/123')
)
const data = await res.json()
expect(res.status).toBe(200)
expect(data).toEqual({
id: '123',
name: 'John Doe'
})
})
```
### POST Request
```typescript
it('should create user', async () => {
const app = new Elysia()
.post('/user', ({ body }) => body)
const res = await app.handle(
new Request('http://localhost/user', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'Jane Doe',
email: 'jane@example.com'
})
})
)
const data = await res.json()
expect(res.status).toBe(200)
expect(data.name).toBe('Jane Doe')
})
```
## Testing Module/Plugin
### Module Structure
```
src/
├── modules/
│ └── auth/
│ ├── index.ts # Elysia instance
│ ├── service.ts
│ └── model.ts
└── index.ts
```
### Auth Module
```typescript
// src/modules/auth/index.ts
import { Elysia, t } from 'elysia'
export const authModule = new Elysia({ prefix: '/auth' })
.post('/login', ({ body, cookie: { session } }) => {
if (body.username === 'admin' && body.password === 'password') {
session.value = 'valid-session'
return { success: true }
}
return { success: false }
}, {
body: t.Object({
username: t.String(),
password: t.String()
})
})
.get('/profile', ({ cookie: { session }, status }) => {
if (!session.value) {
return status(401, { error: 'Unauthorized' })
}
return { username: 'admin' }
})
```
### Auth Module Test
```typescript
// test/auth.test.ts
import { describe, expect, it } from 'bun:test'
import { authModule } from '../src/modules/auth'
describe('Auth Module', () => {
it('should login successfully', async () => {
const res = await authModule.handle(
new Request('http://localhost/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: 'admin',
password: 'password'
})
})
)
const data = await res.json()
expect(res.status).toBe(200)
expect(data.success).toBe(true)
})
it('should reject invalid credentials', async () => {
const res = await authModule.handle(
new Request('http://localhost/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: 'wrong',
password: 'wrong'
})
})
)
const data = await res.json()
expect(data.success).toBe(false)
})
it('should return 401 for unauthenticated profile request', async () => {
const res = await authModule.handle(
new Request('http://localhost/auth/profile')
)
expect(res.status).toBe(401)
})
})
```
## Eden Treaty Testing
### Setup
```typescript
import { treaty } from '@elysiajs/eden'
import { app } from '../src/modules/auth'
const api = treaty(app)
```
### Eden Tests
```typescript
describe('Auth Module with Eden', () => {
it('should login with Eden', async () => {
const { data, error } = await api.auth.login.post({
username: 'admin',
password: 'password'
})
expect(error).toBeNull()
expect(data?.success).toBe(true)
})
it('should get profile with Eden', async () => {
// First login
await api.auth.login.post({
username: 'admin',
password: 'password'
})
// Then get profile
const { data, error } = await api.auth.profile.get()
expect(error).toBeNull()
expect(data?.username).toBe('admin')
})
})
```
## Mocking Dependencies
### With Decorators
```typescript
// app.ts
export const app = new Elysia()
.decorate('db', realDatabase)
.get('/users', ({ db }) => db.getUsers())
// test
import { app } from '../src/app'
describe('App with mocked DB', () => {
it('should use mock database', async () => {
const mockDb = {
getUsers: () => [{ id: 1, name: 'Test User' }]
}
const testApp = app.decorate('db', mockDb)
const res = await testApp.handle(
new Request('http://localhost/users')
)
const data = await res.json()
expect(data).toEqual([{ id: 1, name: 'Test User' }])
})
})
```
## Testing with Headers
```typescript
it('should require authorization', async () => {
const app = new Elysia()
.get('/protected', ({ headers, status }) => {
if (!headers.authorization) {
return status(401)
}
return { data: 'secret' }
})
const res = await app.handle(
new Request('http://localhost/protected', {
headers: {
'Authorization': 'Bearer token123'
}
})
)
expect(res.status).toBe(200)
})
```
## Testing Validation
```typescript
import { Elysia, t } from 'elysia'
it('should validate request body', async () => {
const app = new Elysia()
.post('/user', ({ body }) => body, {
body: t.Object({
name: t.String(),
age: t.Number({ minimum: 0 })
})
})
// Valid request
const validRes = await app.handle(
new Request('http://localhost/user', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'John',
age: 25
})
})
)
expect(validRes.status).toBe(200)
// Invalid request (negative age)
const invalidRes = await app.handle(
new Request('http://localhost/user', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'John',
age: -5
})
})
)
expect(invalidRes.status).toBe(400)
})
```
## Testing WebSocket
```typescript
it('should handle websocket connection', (done) => {
const app = new Elysia()
.ws('/chat', {
message(ws, message) {
ws.send('Echo: ' + message)
}
})
const ws = new WebSocket('ws://localhost:3000/chat')
ws.onopen = () => {
ws.send('Hello')
}
ws.onmessage = (event) => {
expect(event.data).toBe('Echo: Hello')
ws.close()
done()
}
})
```
## Complete Example
```typescript
// src/modules/auth/index.ts
import { Elysia, t } from 'elysia'
export const authModule = new Elysia({ prefix: '/auth' })
.post('/login', ({ body, cookie: { session } }) => {
if (body.username === 'admin' && body.password === 'password') {
session.value = 'valid-session'
return { success: true }
}
return { success: false }
}, {
body: t.Object({
username: t.String(),
password: t.String()
})
})
.get('/profile', ({ cookie: { session }, status }) => {
if (!session.value) {
return status(401)
}
return { username: 'admin' }
})
// test/auth.test.ts
import { describe, expect, it } from 'bun:test'
import { treaty } from '@elysiajs/eden'
import { authModule } from '../src/modules/auth'
const api = treaty(authModule)
describe('Auth Module', () => {
it('should login successfully', async () => {
const { data, error } = await api.auth.login.post({
username: 'admin',
password: 'password'
})
expect(error).toBeNull()
expect(data?.success).toBe(true)
})
it('should return 401 for unauthorized access', async () => {
const { error } = await api.auth.profile.get()
expect(error?.status).toBe(401)
})
})
```

View File

@@ -0,0 +1,491 @@
# Validation Schema - SKILLS.md
## What It Is
Runtime validation + type inference + OpenAPI schema from single source. TypeBox-based with Standard Schema support.
## Basic Usage
```typescript
import { Elysia, t } from 'elysia'
new Elysia()
.get('/id/:id', ({ params: { id } }) => id, {
params: t.Object({ id: t.Number({ minimum: 1 }) }),
response: {
200: t.Number(),
404: t.Literal('Not Found')
}
})
```
## Schema Types
Third parameter of HTTP method:
- **body** - HTTP message
- **query** - URL query params
- **params** - Path params
- **headers** - Request headers
- **cookie** - Request cookies
- **response** - Response (per status)
## Standard Schema Support
Use Zod, Valibot, ArkType, Effect, Yup, Joi:
```typescript
import { z } from 'zod'
import * as v from 'valibot'
.get('/', ({ params, query }) => params.id, {
params: z.object({ id: z.coerce.number() }),
query: v.object({ name: v.literal('Lilith') })
})
```
Mix validators in same handler.
## Body
```typescript
body: t.Object({ name: t.String() })
```
GET/HEAD: body-parser disabled by default (RFC2616).
### File Upload
```typescript
body: t.Object({
file: t.File({ format: 'image/*' }),
multipleFiles: t.Files()
})
// Auto-assumes multipart/form-data
```
### File (Standard Schema)
```typescript
import { fileType } from 'elysia'
body: z.object({
file: z.file().refine((file) => fileType(file, 'image/jpeg'))
})
```
Use `fileType` for security (validates magic number, not just MIME).
## Query
```typescript
query: t.Object({ name: t.String() })
// /?name=Elysia
```
Auto-coerces to specified type.
### Arrays
```typescript
query: t.Object({ name: t.Array(t.String()) })
```
Formats supported:
- **nuqs**: `?name=a,b,c` (comma delimiter)
- **HTML form**: `?name=a&name=b&name=c` (multiple keys)
## Params
```typescript
params: t.Object({ id: t.Number() })
// /id/1
```
Auto-inferred as string if schema not provided.
## Headers
```typescript
headers: t.Object({ authorization: t.String() })
```
`additionalProperties: true` by default. Always lowercase keys.
## Cookie
```typescript
cookie: t.Cookie({
name: t.String()
}, {
secure: true,
httpOnly: true
})
```
Or use `t.Object`. `additionalProperties: true` by default.
## Response
```typescript
response: t.Object({ name: t.String() })
```
### Per Status
```typescript
response: {
200: t.Object({ name: t.String() }),
400: t.Object({ error: t.String() })
}
```
## Error Handling
### Inline Error Property
```typescript
body: t.Object({
x: t.Number({ error: 'x must be number' })
})
```
Or function:
```typescript
x: t.Number({
error({ errors, type, validation, value }) {
return 'Expected x to be number'
}
})
```
### onError Hook
```typescript
.onError(({ code, error }) => {
if (code === 'VALIDATION')
return error.message // or error.all[0].message
})
```
`error.all` - list all error causes. `error.all.find(x => x.path === '/name')` - find specific field.
## Reference Models
Name + reuse models:
```typescript
.model({
sign: t.Object({
username: t.String(),
password: t.String()
})
})
.post('/sign-in', ({ body }) => body, {
body: 'sign',
response: 'sign'
})
```
Extract to plugin:
```typescript
// auth.model.ts
export const authModel = new Elysia().model({ sign: t.Object({...}) })
// main.ts
new Elysia().use(authModel).post('/', ..., { body: 'sign' })
```
### Naming Convention
Prevent duplicates with namespaces:
```typescript
.model({
'auth.admin': t.Object({...}),
'auth.user': t.Object({...})
})
```
Or use `prefix` / `suffix` to rename models in current instance
```typescript
.model({ sign: t.Object({...}) })
.prefix('model', 'auth')
.post('/', () => '', {
body: 'auth.User'
})
```
Models with `prefix` will be capitalized.
## TypeScript Types
```typescript
const MyType = t.Object({ hello: t.Literal('Elysia') })
type MyType = typeof MyType.static
```
Single schema → runtime validation + coercion + TypeScript type + OpenAPI.
## Guard
Apply schema to multiple handlers. Affects all handlers after definition.
### Basic Usage
```typescript
import { Elysia, t } from 'elysia'
new Elysia()
.get('/none', ({ query }) => 'hi')
.guard({
query: t.Object({
name: t.String()
})
})
.get('/query', ({ query }) => query)
.listen(3000)
```
Ensures `query.name` string required for all handlers after guard.
### Behavior
| Path | Response |
|---------------|----------|
| /none | hi |
| /none?name=a | hi |
| /query | error |
| /query?name=a | a |
### Precedence
- Multiple global schemas: latest wins
- Global vs local: local wins
### Schema Types
1. override (default)
Latest schema overrides collided schema.
```typescript
.guard({ query: t.Object({ name: t.String() }) })
.guard({ query: t.Object({ id: t.Number() }) })
// Only id required, name overridden
```
2. standalone
Both schemas run independently. Both validated.
```typescript
.guard({ query: t.Object({ name: t.String() }) }, { type: 'standalone' })
.guard({ query: t.Object({ id: t.Number() }) }, { type: 'standalone' })
// Both name AND id required
```
# Typebox Validation (Elysia.t)
Elysia.t = TypeBox with server-side pre-configuration + HTTP-specific types
**TypeBox API mirrors TypeScript syntax** but provides runtime validation
## Basic Types
| TypeBox | TypeScript | Example Value |
|---------|------------|---------------|
| `t.String()` | `string` | `"hello"` |
| `t.Number()` | `number` | `42` |
| `t.Boolean()` | `boolean` | `true` |
| `t.Array(t.Number())` | `number[]` | `[1, 2, 3]` |
| `t.Object({ x: t.Number() })` | `{ x: number }` | `{ x: 10 }` |
| `t.Null()` | `null` | `null` |
| `t.Literal(42)` | `42` | `42` |
## Attributes (JSON Schema 7)
```ts
// Email format
t.String({ format: 'email' })
// Number constraints
t.Number({ minimum: 10, maximum: 100 })
// Array constraints
t.Array(t.Number(), {
minItems: 1, // min items
maxItems: 5 // max items
})
// Object - allow extra properties
t.Object(
{ x: t.Number() },
{ additionalProperties: true } // default: false
)
```
## Common Patterns
### Union (Multiple Types)
```ts
t.Union([t.String(), t.Number()])
// type: string | number
// values: "Hello" or 123
```
### Optional (Field Optional)
```ts
t.Object({
x: t.Number(),
y: t.Optional(t.Number()) // can be undefined
})
// type: { x: number, y?: number }
// value: { x: 123 } or { x: 123, y: 456 }
```
### Partial (All Fields Optional)
```ts
t.Partial(t.Object({
x: t.Number(),
y: t.Number()
}))
// type: { x?: number, y?: number }
// value: {} or { y: 123 } or { x: 1, y: 2 }
```
## Elysia-Specific Types
### UnionEnum (One of Values)
```ts
t.UnionEnum(['rapi', 'anis', 1, true, false])
```
### File (Single File Upload)
```ts
t.File({
type: 'image', // or ['image', 'video']
minSize: '1k', // 1024 bytes
maxSize: '5m' // 5242880 bytes
})
```
**File unit suffixes**:
- `m` = MegaByte (1048576 bytes)
- `k` = KiloByte (1024 bytes)
### Files (Multiple Files)
```ts
t.Files() // extends File + array
```
### Cookie (Cookie Jar)
```ts
t.Cookie({
name: t.String()
}, {
secrets: 'secret-key' // or ['key1', 'key2'] for rotation
})
```
### Nullable (Allow null)
```ts
t.Nullable(t.String())
// type: string | null
```
### MaybeEmpty (Allow null + undefined)
```ts
t.MaybeEmpty(t.String())
// type: string | null | undefined
```
### Form (FormData Validation)
```ts
t.Form({
someValue: t.File()
})
// Syntax sugar for t.Object with FormData support
```
### UInt8Array (Buffer → Uint8Array)
```ts
t.UInt8Array()
// For binary file uploads with arrayBuffer parser
```
### ArrayBuffer (Buffer → ArrayBuffer)
```ts
t.ArrayBuffer()
// For binary file uploads with arrayBuffer parser
```
### ObjectString (String → Object)
```ts
t.ObjectString()
// Accepts: '{"x":1}' → parses to { x: 1 }
// Use in: query string, headers, FormData
```
### BooleanString (String → Boolean)
```ts
t.BooleanString()
// Accepts: 'true'/'false' → parses to boolean
// Use in: query string, headers, FormData
```
### Numeric (String/Number → Number)
```ts
t.Numeric()
// Accepts: '123' or 123 → transforms to 123
// Use in: path params, query string
```
## Elysia Behavior Differences from TypeBox
### 1. Optional Behavior
In Elysia, `t.Optional` makes **entire route parameter** optional (not object field):
```ts
.get('/optional', ({ query }) => query, {
query: t.Optional( // makes query itself optional
t.Object({ name: t.String() })
)
})
```
**Different from TypeBox**: TypeBox uses Optional for object fields only
### 2. Number → Numeric Auto-Conversion
**Route schema only** (not nested objects):
```ts
.get('/:id', ({ id }) => id, {
params: t.Object({
id: t.Number() // ✅ Auto-converts to t.Numeric()
}),
body: t.Object({
id: t.Number() // ❌ NOT converted (stays t.Number())
})
})
// Outside route schema
t.Number() // ❌ NOT converted
```
**Why**: HTTP headers/query/params always strings. Auto-conversion parses numeric strings.
### 3. Boolean → BooleanString Auto-Conversion
Same as Number → Numeric:
```ts
.get('/:active', ({ active }) => active, {
params: t.Object({
active: t.Boolean() // ✅ Auto-converts to t.BooleanString()
}),
body: t.Object({
active: t.Boolean() // ❌ NOT converted
})
})
```
## Usage Pattern
```ts
import { Elysia, t } from 'elysia'
new Elysia()
.post('/', ({ body }) => `Hello ${body}`, {
body: t.String() // validates body is string
})
.listen(3000)
```
**Validation flow**:
1. Request arrives
2. Schema validates against HTTP body/params/query/headers
3. If valid → handler executes
4. If invalid → Error Life Cycle
## Notes
[Inference] Based on docs:
- TypeBox mirrors TypeScript but adds runtime validation
- Elysia.t extends TypeBox with HTTP-specific types
- Auto-conversion (Number→Numeric, Boolean→BooleanString) only for route schemas
- Use `t.Optional` for optional route params (different from TypeBox behavior)
- File validation supports unit suffixes ('1k', '5m')
- ObjectString/BooleanString for parsing strings in query/headers
- Cookie supports key rotation with array of secrets

View File

@@ -0,0 +1,250 @@
# WebSocket
## Basic WebSocket
```typescript
import { Elysia } from 'elysia'
new Elysia()
.ws('/chat', {
message(ws, message) {
ws.send(message) // Echo back
}
})
.listen(3000)
```
## With Validation
```typescript
import { Elysia, t } from 'elysia'
.ws('/chat', {
body: t.Object({
message: t.String(),
username: t.String()
}),
response: t.Object({
message: t.String(),
timestamp: t.Number()
}),
message(ws, body) {
ws.send({
message: body.message,
timestamp: Date.now()
})
}
})
```
## Lifecycle Events
```typescript
.ws('/chat', {
open(ws) {
console.log('Client connected')
},
message(ws, message) {
console.log('Received:', message)
ws.send('Echo: ' + message)
},
close(ws) {
console.log('Client disconnected')
},
error(ws, error) {
console.error('Error:', error)
}
})
```
## Broadcasting
```typescript
const connections = new Set<any>()
.ws('/chat', {
open(ws) {
connections.add(ws)
},
message(ws, message) {
// Broadcast to all connected clients
for (const client of connections) {
client.send(message)
}
},
close(ws) {
connections.delete(ws)
}
})
```
## With Authentication
```typescript
.ws('/chat', {
beforeHandle({ headers, status }) {
const token = headers.authorization?.replace('Bearer ', '')
if (!verifyToken(token)) {
return status(401)
}
},
message(ws, message) {
ws.send(message)
}
})
```
## Room-Based Chat
```typescript
const rooms = new Map<string, Set<any>>()
.ws('/chat/:room', {
open(ws) {
const room = ws.data.params.room
if (!rooms.has(room)) {
rooms.set(room, new Set())
}
rooms.get(room)!.add(ws)
},
message(ws, message) {
const room = ws.data.params.room
const clients = rooms.get(room)
if (clients) {
for (const client of clients) {
client.send(message)
}
}
},
close(ws) {
const room = ws.data.params.room
const clients = rooms.get(room)
if (clients) {
clients.delete(ws)
if (clients.size === 0) {
rooms.delete(room)
}
}
}
})
```
## With State/Context
```typescript
.ws('/chat', {
open(ws) {
ws.data.userId = generateUserId()
ws.data.joinedAt = Date.now()
},
message(ws, message) {
const response = {
userId: ws.data.userId,
message,
timestamp: Date.now()
}
ws.send(response)
}
})
```
## Client Usage (Browser)
```typescript
const ws = new WebSocket('ws://localhost:3000/chat')
ws.onopen = () => {
console.log('Connected')
ws.send('Hello Server!')
}
ws.onmessage = (event) => {
console.log('Received:', event.data)
}
ws.onerror = (error) => {
console.error('Error:', error)
}
ws.onclose = () => {
console.log('Disconnected')
}
```
## Eden Treaty WebSocket
```typescript
// Server
export const app = new Elysia()
.ws('/chat', {
message(ws, message) {
ws.send(message)
}
})
export type App = typeof app
// Client
import { treaty } from '@elysiajs/eden'
import type { App } from './server'
const api = treaty<App>('localhost:3000')
const chat = api.chat.subscribe()
chat.subscribe((message) => {
console.log('Received:', message)
})
chat.send('Hello!')
```
## Headers in WebSocket
```typescript
.ws('/chat', {
header: t.Object({
authorization: t.String()
}),
beforeHandle({ headers, status }) {
const token = headers.authorization?.replace('Bearer ', '')
if (!token) return status(401)
},
message(ws, message) {
ws.send(message)
}
})
```
## Query Parameters
```typescript
.ws('/chat', {
query: t.Object({
username: t.String()
}),
message(ws, message) {
const username = ws.data.query.username
ws.send(`${username}: ${message}`)
}
})
// Client
const ws = new WebSocket('ws://localhost:3000/chat?username=john')
```
## Compression
```typescript
new Elysia({
websocket: {
perMessageDeflate: true
}
})
.ws('/chat', {
message(ws, message) {
ws.send(message)
}
})
```

View File

@@ -0,0 +1,133 @@
---
name: mantine-dev
description: "Mantine UI library for React: 100+ components, hooks, forms, theming, dark mode, CSS modules, and Vite/TypeScript setup. Use when building React applications with Mantine components, configuring theming/dark mode, or working with Mantine hooks and forms. Keywords: Mantine, React, UI components, CSS modules, theming."
metadata:
version: "8.3.18"
release_date: "2026-03-17"
---
# Mantine UI Library
Mantine is a fully-featured React components library with TypeScript support. It provides 100+ hooks and components with native dark mode, CSS-in-JS via CSS modules, and excellent accessibility.
## Focus
This skill focuses on:
- **Vite** + **TypeScript** setup (not Next.js or CRA)
- CSS modules with PostCSS preset
- Vitest for testing
- ESLint with eslint-config-mantine
## Installation
See `references/getting-started.md` for Vite template setup, manual installation, and optional packages.
## PostCSS Configuration
Create `postcss.config.cjs`:
```js
module.exports = {
plugins: {
"postcss-preset-mantine": {},
"postcss-simple-vars": {
variables: {
"mantine-breakpoint-xs": "36em",
"mantine-breakpoint-sm": "48em",
"mantine-breakpoint-md": "62em",
"mantine-breakpoint-lg": "75em",
"mantine-breakpoint-xl": "88em",
},
},
},
};
```
## App Setup
```tsx
// src/App.tsx
import "@mantine/core/styles.css";
// Other style imports as needed:
// import '@mantine/dates/styles.css';
// import '@mantine/notifications/styles.css';
import { MantineProvider, createTheme } from "@mantine/core";
const theme = createTheme({
// Theme customization here
});
function App() {
return <MantineProvider theme={theme}>{/* Your app */}</MantineProvider>;
}
```
## Critical Prohibitions
- Do NOT skip MantineProvider wrapper — all components require it
- Do NOT forget to import `@mantine/core/styles.css` — components won't style without it
- Do NOT mix Mantine with other UI libraries (e.g., Chakra, MUI) in same project
- Do NOT use inline styles for theme values — use CSS variables or theme object
- Do NOT skip PostCSS setup — responsive mixins won't work
- Do NOT forget `key={form.key('path')}` when using uncontrolled forms
## Core Concepts
### 1. MantineProvider
Wraps your app, provides theme context and color scheme management.
### 2. Theme Object
Customize colors, typography, spacing, component default props.
### 3. Style Props
All components accept style props like `mt`, `p`, `c`, `bg`, etc.
### 4. CSS Variables
All theme values exposed as CSS variables (e.g., `--mantine-color-blue-6`).
### 5. Polymorphic Components
Many components support `component` prop to render as different elements.
## Definition of Done
- [ ] MantineProvider wraps the app
- [ ] Styles imported (`@mantine/core/styles.css`)
- [ ] PostCSS configured with mantine-preset
- [ ] Theme customization in createTheme
- [ ] Color scheme (light/dark) handled
- [ ] TypeScript types working
- [ ] Tests pass with Vitest + custom render
## References (Detailed Guides)
### Setup & Configuration
- [getting-started.md](references/getting-started.md) — Installation, Vite setup, project structure
- [styling.md](references/styling.md) — MantineProvider, theme, CSS modules, style props, dark mode
### Core Features
- [components.md](references/components.md) — Core UI components patterns
- [hooks.md](references/hooks.md) — @mantine/hooks utility hooks
- [forms.md](references/forms.md) — @mantine/form, useForm, validation
### Development
- [testing.md](references/testing.md) — Vitest setup, custom render, mocking
- [eslint.md](references/eslint.md) — eslint-config-mantine setup
## Links
- [Documentation](https://mantine.dev)
- [Releases](https://github.com/mantinedev/mantine/releases)
- [GitHub](https://github.com/mantinedev/mantine)
- [npm](https://www.npmjs.com/package/@mantine/core)
- [Vite template](https://github.com/mantinedev/vite-template)
- [LLM docs](https://mantine.dev/llms.txt)

View File

@@ -0,0 +1,258 @@
# Components Reference
`@mantine/core` provides 120+ components. This reference covers key patterns.
## Layout Components
### Container, Stack, Group, Flex
```tsx
import { Container, Stack, Group, Flex } from '@mantine/core';
<Container size="md">{/* Centers content, max-width */}</Container>
<Stack gap="md">{/* Vertical flex */}</Stack>
<Group gap="sm" justify="space-between">{/* Horizontal flex */}</Group>
<Flex direction="column" gap="md" align="center">{/* Generic flex */}</Flex>
```
### Grid & SimpleGrid
```tsx
import { Grid, SimpleGrid } from '@mantine/core';
// CSS Grid with responsive spans
<Grid>
<Grid.Col span={{ base: 12, sm: 6, lg: 4 }}>Responsive</Grid.Col>
</Grid>
// Equal-width columns
<SimpleGrid cols={{ base: 1, sm: 2, lg: 4 }}>{/* Items */}</SimpleGrid>
```
## Button Variants
```tsx
import { Button, ActionIcon } from '@mantine/core';
<Button variant="filled">Default</Button>
<Button variant="outline">Outline</Button>
<Button variant="light">Light</Button>
<Button variant="subtle">Subtle</Button>
<Button variant="white">White</Button>
<Button loading>Loading state</Button>
<Button leftSection={<IconPlus />}>With Icon</Button>
// Icon button
<ActionIcon variant="filled" color="blue"><IconSettings /></ActionIcon>
```
## Inputs Pattern
All inputs follow consistent API:
```tsx
import { TextInput, PasswordInput, Textarea, NumberInput, Select } from '@mantine/core';
// Common props: label, description, error, required, placeholder
<TextInput
label="Email"
description="We won't share it"
error="Invalid email"
required
withAsterisk
/>
<Select
label="Country"
data={['USA', 'Canada']}
searchable
clearable
/>
// Objects with value/label
<Select data={[{ value: 'us', label: 'United States' }]} />
```
## Overlays Pattern
Modals, Drawers, Menus, Popovers all use similar pattern:
```tsx
import { Modal, Drawer, Menu, Popover } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
// Common pattern with useDisclosure
const [opened, { open, close }] = useDisclosure(false);
// Modal
<Modal opened={opened} onClose={close} title="Title">Content</Modal>
// Drawer
<Drawer opened={opened} onClose={close} position="left">Navigation</Drawer>
// Menu (dropdown)
<Menu>
<Menu.Target><Button>Toggle</Button></Menu.Target>
<Menu.Dropdown>
<Menu.Item leftSection={<IconSettings />}>Settings</Menu.Item>
<Menu.Divider />
<Menu.Item color="red">Delete</Menu.Item>
</Menu.Dropdown>
</Menu>
// Popover
<Popover width={200} withArrow>
<Popover.Target><Button>Info</Button></Popover.Target>
<Popover.Dropdown>Details here</Popover.Dropdown>
</Popover>
```
## Feedback Components
```tsx
import { Loader, Alert, Notification, Progress, Skeleton } from '@mantine/core';
<Loader type="bars" /> // oval, bars, dots
<Alert variant="light" color="blue" title="Info">Message</Alert>
<Notification title="Success" color="green" icon={<IconCheck />}>
Saved!
</Notification>
<Progress value={65} />
<Progress.Root size="xl">
<Progress.Section value={35} color="cyan"><Progress.Label>Docs</Progress.Label></Progress.Section>
</Progress.Root>
// Loading placeholders
<Skeleton height={50} circle />
<Skeleton height={8} radius="xl" />
```
## Typography
```tsx
import { Title, Text, Anchor, Highlight, Code } from '@mantine/core';
<Title order={1}>h1 heading</Title>
<Title order={2} c="dimmed">h2 dimmed</Title>
<Text size="sm" c="dimmed" fw={700}>Small bold dimmed</Text>
<Text truncate>Long text...</Text>
<Text lineClamp={2}>Multi-line truncate...</Text>
<Highlight highlight={['react', 'mantine']}>
Learn React with Mantine
</Highlight>
<Code>inline</Code>
<Code block>{`const x = 1;`}</Code>
```
## Data Display
```tsx
import { Badge, Card, Table, Avatar, Image, Tabs, Accordion } from '@mantine/core';
// Badge variants
<Badge>Default</Badge>
<Badge variant="dot" color="red">Dot</Badge>
// Card with sections
<Card shadow="sm" padding="lg" withBorder>
<Card.Section><Image src="/img.jpg" height={160} /></Card.Section>
<Text>Content</Text>
</Card>
// Table
<Table striped highlightOnHover withTableBorder>
<Table.Thead><Table.Tr><Table.Th>Name</Table.Th></Table.Tr></Table.Thead>
<Table.Tbody><Table.Tr><Table.Td>John</Table.Td></Table.Tr></Table.Tbody>
</Table>
// Tabs
<Tabs defaultValue="tab1">
<Tabs.List>
<Tabs.Tab value="tab1">First</Tabs.Tab>
<Tabs.Tab value="tab2">Second</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="tab1">Content 1</Tabs.Panel>
</Tabs>
// Accordion
<Accordion defaultValue="item-1">
<Accordion.Item value="item-1">
<Accordion.Control>Section 1</Accordion.Control>
<Accordion.Panel>Content</Accordion.Panel>
</Accordion.Item>
</Accordion>
```
## Navigation
```tsx
import { NavLink, Pagination, Stepper, Breadcrumbs } from '@mantine/core';
<NavLink href="#" label="Dashboard" leftSection={<IconHome />} active />
<NavLink label="Settings">
<NavLink label="General" />
<NavLink label="Security" />
</NavLink>
<Pagination total={10} value={page} onChange={setPage} />
<Stepper active={active}>
<Stepper.Step label="Step 1">Content 1</Stepper.Step>
<Stepper.Step label="Step 2">Content 2</Stepper.Step>
<Stepper.Completed>Done!</Stepper.Completed>
</Stepper>
```
## Common Style Props
All components accept these props:
```tsx
<Component
// Margin & Padding
m="md" mt="xs" p="sm" px="md"
// Colors
c="dimmed" bg="blue.1"
// Typography
fw={500} fz="sm"
// Dimensions
w={200} h="100%" maw={500}
// Responsive
p={{ base: 'xs', sm: 'md', lg: 'xl' }}
/>
```
## Polymorphic Components
Render as different elements:
```tsx
import { Button } from '@mantine/core';
import { Link } from 'react-router-dom';
<Button component={Link} to="/about">Link Button</Button>
<Button component="a" href="https://example.com">Anchor Button</Button>
```
## Visibility Props
```tsx
<Text hiddenFrom="sm">Hidden on sm+</Text>
<Text visibleFrom="md">Visible on md+</Text>
<Text lightHidden>Only in dark mode</Text>
<Text darkHidden>Only in light mode</Text>
```

View File

@@ -0,0 +1,269 @@
# ESLint Configuration Reference
`eslint-config-mantine` provides ESLint rules and configurations used in Mantine projects.
## Installation
```bash
npm install -D @eslint/js eslint eslint-plugin-jsx-a11y eslint-plugin-react typescript-eslint eslint-config-mantine
```
## Configuration
Create `eslint.config.js` (ESLint flat config):
```js
import mantine from 'eslint-config-mantine';
import tseslint from 'typescript-eslint';
export default [
...tseslint.configs.recommended,
...mantine,
{
ignores: ['**/*.{mjs,cjs,js,d.ts,d.mts}'],
},
{
files: ['**/*.story.tsx'],
rules: {
'no-console': 'off',
},
},
{
languageOptions: {
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: process.cwd(),
},
},
},
];
```
## What's Included
eslint-config-mantine includes:
### TypeScript Rules
- `typescript-eslint/recommended` base rules
- Strict type checking
- No unused variables (except with `_` prefix)
- No explicit `any` (warning)
### React Rules
- React hooks rules (exhaustive-deps, rules-of-hooks)
- JSX-specific rules
- No unknown properties
- Self-closing components
### Accessibility (a11y)
- `eslint-plugin-jsx-a11y` rules
- Alt text requirements
- ARIA attribute validation
- Interactive element handling
- Focus management rules
### Import/Export
- Import order organization
- No duplicate imports
- No unresolved imports
### General
- No console.log (warning)
- Consistent code style
- No debugger
## Script Setup
Add to `package.json`:
```json
{
"scripts": {
"lint": "npm run eslint && npm run stylelint",
"eslint": "eslint . --cache",
"eslint:fix": "eslint . --cache --fix"
}
}
```
## Common Configuration Adjustments
### Allow console in specific files
```js
export default [
...mantine,
{
files: ['**/*.test.tsx', '**/*.story.tsx'],
rules: {
'no-console': 'off',
},
},
];
```
### Disable specific rules
```js
export default [
...mantine,
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'react/jsx-no-target-blank': 'off',
},
},
];
```
### Add custom rules
```js
export default [
...mantine,
{
rules: {
'prefer-const': 'error',
'no-var': 'error',
'object-shorthand': 'error',
},
},
];
```
### Configure for monorepo
```js
export default [
...mantine,
{
languageOptions: {
parserOptions: {
project: ['./tsconfig.json', './packages/*/tsconfig.json'],
tsconfigRootDir: process.cwd(),
},
},
},
];
```
## Integration with Prettier
If using Prettier, add `eslint-config-prettier`:
```bash
npm install -D eslint-config-prettier
```
```js
import mantine from 'eslint-config-mantine';
import prettier from 'eslint-config-prettier';
export default [
...mantine,
prettier, // Must be last to override conflicting rules
];
```
## VS Code Integration
Install ESLint extension, then add to `.vscode/settings.json`:
```json
{
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
}
```
## Stylelint (Optional)
For CSS linting, the Mantine Vite template also includes Stylelint:
```bash
npm install -D stylelint stylelint-config-standard-scss
```
Create `stylelint.config.js`:
```js
export default {
extends: ['stylelint-config-standard-scss'],
rules: {
'selector-class-pattern': null,
},
};
```
Add to scripts:
```json
{
"scripts": {
"stylelint": "stylelint '**/*.css' --cache"
}
}
```
## Common ESLint Errors & Fixes
### React Hook Dependency Warning
```tsx
// Warning: React Hook useEffect has missing dependency
useEffect(() => {
fetchData(id);
}, []); // Missing 'id' and 'fetchData'
// Fix: Add dependencies
useEffect(() => {
fetchData(id);
}, [id, fetchData]);
// Or use useCallback for functions
const fetchData = useCallback((id) => { /* ... */ }, []);
```
### Unused Variable
```tsx
// Error: 'x' is defined but never used
const x = 5;
// Fix: Prefix with underscore if intentionally unused
const _x = 5;
```
### Missing Key Prop
```tsx
// Error: Missing "key" prop
items.map((item) => <Item>{item.name}</Item>);
// Fix: Add unique key
items.map((item) => <Item key={item.id}>{item.name}</Item>);
```
### Accessibility Issues
```tsx
// Error: img elements must have an alt prop
<img src="photo.jpg" />
// Fix: Add alt text
<img src="photo.jpg" alt="Description" />
// Decorative image
<img src="decoration.jpg" alt="" role="presentation" />
```
## Template Configuration
The [Mantine Vite template](https://github.com/mantinedev/vite-template) includes a complete ESLint + Prettier + Stylelint setup that you can use as reference.

View File

@@ -0,0 +1,496 @@
# Forms Reference
`@mantine/form` provides `useForm` hook for managing form state, validation, and submission.
## Installation
```bash
npm install @mantine/form
```
No styles needed — works with or without `@mantine/core`.
## Basic Usage
```tsx
import { useForm } from '@mantine/form';
import { TextInput, Button, Box } from '@mantine/core';
interface FormValues {
email: string;
name: string;
}
function Demo() {
const form = useForm<FormValues>({
mode: 'uncontrolled', // Recommended for performance
initialValues: {
email: '',
name: '',
},
validate: {
email: (value) => (/^\S+@\S+$/.test(value) ? null : 'Invalid email'),
name: (value) => (value.length < 2 ? 'Name too short' : null),
},
});
return (
<Box component="form" onSubmit={form.onSubmit((values) => console.log(values))}>
<TextInput
label="Name"
placeholder="Your name"
key={form.key('name')}
{...form.getInputProps('name')}
/>
<TextInput
label="Email"
placeholder="your@email.com"
key={form.key('email')}
{...form.getInputProps('email')}
/>
<Button type="submit" mt="md">Submit</Button>
</Box>
);
}
```
## useForm Options
```tsx
interface UseFormInput<Values> {
mode?: 'controlled' | 'uncontrolled'; // Default: 'controlled'
initialValues?: Values;
initialErrors?: FormErrors;
initialDirty?: Record<string, boolean>;
initialTouched?: Record<string, boolean>;
validate?: FormValidation<Values>;
validateInputOnChange?: boolean | string[];
validateInputOnBlur?: boolean | string[];
clearInputErrorOnChange?: boolean;
onValuesChange?: (values: Values, previous: Values) => void;
onSubmitPreventDefault?: 'always' | 'never' | 'validation-failed';
}
```
## Controlled vs Uncontrolled Mode
### Uncontrolled (Recommended)
Better performance — values stored in DOM:
```tsx
const form = useForm({
mode: 'uncontrolled', // Add mode
initialValues: { name: '' },
});
<TextInput
key={form.key('name')} // Required for uncontrolled
{...form.getInputProps('name')}
/>
```
### Controlled
Values stored in React state — re-renders on every change:
```tsx
const form = useForm({
mode: 'controlled',
initialValues: { name: '' },
});
<TextInput {...form.getInputProps('name')} />
```
## Form Values
```tsx
// Get all values
const values = form.getValues();
// Set single field
form.setFieldValue('email', 'new@email.com');
// Set multiple values
form.setValues({ name: 'John', email: 'john@email.com' });
// Set values from previous state
form.setValues((prev) => ({ ...prev, name: 'Updated' }));
// Reset to initialValues
form.reset();
// Reset single field
form.resetField('email');
// Update initialValues (affects reset)
form.setInitialValues({ name: 'New Initial' });
```
## Validation
### Inline Rules
```tsx
const form = useForm({
mode: 'uncontrolled',
initialValues: {
email: '',
age: 0,
password: '',
confirmPassword: '',
},
validate: {
email: (value) => (/^\S+@\S+$/.test(value) ? null : 'Invalid email'),
age: (value) => (value < 18 ? 'Must be 18+' : null),
// Access all values for cross-field validation
confirmPassword: (value, values) =>
value !== values.password ? 'Passwords do not match' : null,
},
});
```
### Function-based Validation
```tsx
const form = useForm({
mode: 'uncontrolled',
initialValues: { name: '', email: '' },
validate: (values) => ({
name: values.name.length < 2 ? 'Name too short' : null,
email: !values.email.includes('@') ? 'Invalid email' : null,
}),
});
```
### Schema Validation (Zod, Yup, Joi)
```tsx
import { zodResolver } from 'mantine-form-zod-resolver';
import { z } from 'zod';
const schema = z.object({
name: z.string().min(2, 'Name must have at least 2 characters'),
email: z.string().email('Invalid email'),
age: z.number().min(18, 'Must be 18 or older'),
});
const form = useForm({
mode: 'uncontrolled',
initialValues: { name: '', email: '', age: 0 },
validate: zodResolver(schema),
});
```
Install resolver:
```bash
npm install mantine-form-zod-resolver zod
# or
npm install mantine-form-yup-resolver yup
# or
npm install mantine-form-joi-resolver joi
```
### Validation Timing
```tsx
const form = useForm({
mode: 'uncontrolled',
validateInputOnChange: true, // Validate all on change
validateInputOnBlur: true, // Validate all on blur
clearInputErrorOnChange: true, // Clear error when value changes (default)
});
// Validate specific fields only
const form = useForm({
mode: 'uncontrolled',
validateInputOnChange: ['email', 'password'],
validateInputOnBlur: ['email'],
});
```
### Manual Validation
```tsx
// Validate all fields
const result = form.validate();
// result.hasErrors: boolean
// result.errors: FormErrors
// Validate single field
form.validateField('email');
// Check if valid (without setting errors)
const isValid = form.isValid();
const isEmailValid = form.isValid('email');
```
## Errors
```tsx
// Get current errors
form.errors; // { email: 'Invalid', name: null }
// Set error
form.setFieldError('email', 'This email is taken');
// Set multiple errors
form.setErrors({ email: 'Invalid', name: 'Required' });
// Clear all errors
form.clearErrors();
// Clear single field error
form.clearFieldError('email');
```
## Form Submission
```tsx
<form
onSubmit={form.onSubmit(
// Success handler - called when validation passes
(values, event) => {
console.log('Valid:', values);
// Submit to API
},
// Error handler - called when validation fails
(errors, values, event) => {
console.log('Errors:', errors);
// Show notification, focus first error, etc.
}
)}
>
{/* inputs */}
</form>
```
## Nested Objects
```tsx
const form = useForm({
mode: 'uncontrolled',
initialValues: {
user: {
firstName: '',
lastName: '',
address: {
city: '',
country: '',
},
},
},
validate: {
user: {
firstName: (value) => (value.length < 2 ? 'Too short' : null),
address: {
city: (value) => (!value ? 'Required' : null),
},
},
},
});
<TextInput
key={form.key('user.firstName')}
{...form.getInputProps('user.firstName')}
/>
<TextInput
key={form.key('user.address.city')}
{...form.getInputProps('user.address.city')}
/>
```
## List Fields
```tsx
const form = useForm({
mode: 'uncontrolled',
initialValues: {
employees: [
{ name: '', email: '' },
],
},
});
// Add item
form.insertListItem('employees', { name: '', email: '' });
// Add at specific index
form.insertListItem('employees', { name: '', email: '' }, 0);
// Remove item
form.removeListItem('employees', 1);
// Replace item
form.replaceListItem('employees', 0, { name: 'New', email: 'new@email.com' });
// Reorder items
form.reorderListItem('employees', { from: 0, to: 2 });
```
### Rendering List
```tsx
import { FORM_INDEX } from '@mantine/form';
function Demo() {
const fields = form.getValues().employees.map((item, index) => (
<Group key={item.key}>
<TextInput
key={form.key(`employees.${index}.name`)}
{...form.getInputProps(`employees.${index}.name`)}
/>
<TextInput
key={form.key(`employees.${index}.email`)}
{...form.getInputProps(`employees.${index}.email`)}
/>
<ActionIcon onClick={() => form.removeListItem('employees', index)}>
<IconTrash />
</ActionIcon>
</Group>
));
return (
<Box>
{fields}
<Button onClick={() => form.insertListItem('employees', { name: '', email: '' })}>
Add Employee
</Button>
</Box>
);
}
// Validation for list items with FORM_INDEX
const form = useForm({
mode: 'uncontrolled',
validateInputOnChange: [`employees.${FORM_INDEX}.name`],
});
```
## Touched & Dirty State
```tsx
// Check if any field was interacted with
form.isTouched();
form.isTouched('email');
// Check if values differ from initialValues
form.isDirty();
form.isDirty('email');
// Set touched state
form.setTouched({ email: true, name: false });
form.resetTouched();
// Set dirty state
form.setDirty({ email: true });
form.resetDirty(); // Snapshot current values as "clean"
```
## Form Context
Share form across components without prop drilling:
```tsx
// formContext.ts
import { createFormContext } from '@mantine/form';
interface FormValues {
name: string;
email: string;
}
export const [FormProvider, useFormContext, useForm] = createFormContext<FormValues>();
```
```tsx
// Parent component
import { FormProvider, useForm } from './formContext';
function Parent() {
const form = useForm({
mode: 'uncontrolled',
initialValues: { name: '', email: '' },
});
return (
<FormProvider form={form}>
<NameInput />
<EmailInput />
</FormProvider>
);
}
// Child component
import { useFormContext } from './formContext';
function NameInput() {
const form = useFormContext();
return (
<TextInput
key={form.key('name')}
{...form.getInputProps('name')}
/>
);
}
```
## UseFormReturnType
Type for passing form as prop:
```tsx
import { UseFormReturnType } from '@mantine/form';
interface Props {
form: UseFormReturnType<{ name: string; email: string }>;
}
function NameField({ form }: Props) {
return (
<TextInput
key={form.key('name')}
{...form.getInputProps('name')}
/>
);
}
```
## Built-in Validators
```tsx
import { isNotEmpty, isEmail, hasLength, matches, isInRange } from '@mantine/form';
const form = useForm({
mode: 'uncontrolled',
initialValues: {
name: '',
email: '',
age: 0,
website: '',
terms: false,
},
validate: {
name: isNotEmpty('Name is required'),
email: isEmail('Invalid email'),
age: isInRange({ min: 18, max: 99 }, 'Age must be 18-99'),
website: matches(/^https?:\/\//, 'Must start with http'),
terms: isNotEmpty('Must accept terms'),
},
});
```
## Focus First Error
```tsx
<form
onSubmit={form.onSubmit(
(values) => { /* success */ },
(errors) => {
const firstErrorPath = Object.keys(errors)[0];
form.getInputNode(firstErrorPath)?.focus();
}
)}
>
```

View File

@@ -0,0 +1,215 @@
# Getting Started Reference
## Vite Template (Recommended)
The fastest way to start — official template includes everything:
```bash
# Clone template
git clone https://github.com/mantinedev/vite-template my-app
cd my-app
# Install dependencies
yarn install # or npm install
# Start development
yarn dev
```
### Template Features
- PostCSS with `postcss-preset-mantine`
- TypeScript configured
- Storybook setup
- Vitest with React Testing Library
- ESLint with `eslint-config-mantine`
- Prettier configured
## Manual Setup
### 1. Create Vite Project
```bash
npm create vite@latest my-app -- --template react-ts
cd my-app
```
### 2. Install Packages
```bash
# Core (required)
npm install @mantine/core @mantine/hooks
# PostCSS (required for responsive styles)
npm install -D postcss postcss-preset-mantine postcss-simple-vars
# Optional packages
npm install @mantine/form # Forms with validation
npm install @mantine/dates dayjs # Date/time components
npm install @mantine/notifications # Toast notifications
npm install @mantine/modals # Modal manager
npm install @mantine/charts recharts # Charts
npm install @mantine/dropzone # File upload
npm install @mantine/spotlight # Command palette (Cmd+K)
npm install @mantine/code-highlight # Code syntax highlighting
npm install @mantine/carousel embla-carousel-react # Carousel
npm install @mantine/tiptap @tiptap/react @tiptap/pm @tiptap/starter-kit # Rich text editor
```
### 3. Configure PostCSS
Create `postcss.config.cjs`:
```js
module.exports = {
plugins: {
'postcss-preset-mantine': {},
'postcss-simple-vars': {
variables: {
'mantine-breakpoint-xs': '36em',
'mantine-breakpoint-sm': '48em',
'mantine-breakpoint-md': '62em',
'mantine-breakpoint-lg': '75em',
'mantine-breakpoint-xl': '88em',
},
},
},
};
```
### 4. Import Styles
In your entry file (e.g., `src/main.tsx` or `src/App.tsx`):
```tsx
// Required - core styles
import '@mantine/core/styles.css';
// Package-specific styles (import only what you use)
import '@mantine/dates/styles.css';
import '@mantine/notifications/styles.css';
import '@mantine/code-highlight/styles.css';
import '@mantine/dropzone/styles.css';
import '@mantine/spotlight/styles.css';
import '@mantine/carousel/styles.css';
import '@mantine/tiptap/styles.css';
// Note: @mantine/form and @mantine/hooks have no styles
```
### 5. Setup MantineProvider
```tsx
// src/App.tsx
import '@mantine/core/styles.css';
import { MantineProvider, createTheme } from '@mantine/core';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
const theme = createTheme({
// Your theme customization
primaryColor: 'blue',
fontFamily: 'Inter, sans-serif',
});
function App() {
return (
<MantineProvider theme={theme}>
<BrowserRouter>
<Routes>
{/* Your routes */}
</Routes>
</BrowserRouter>
</MantineProvider>
);
}
export default App;
```
## Project Structure
Recommended structure for Mantine projects:
```
src/
├── components/
│ ├── Layout/
│ │ ├── AppShell.tsx
│ │ ├── Header.tsx
│ │ └── Navbar.tsx
│ └── ui/ # Custom UI components
├── pages/
│ ├── Home.tsx
│ └── Settings.tsx
├── hooks/ # Custom hooks
├── theme/
│ ├── index.ts # createTheme export
│ └── components.ts # Component default props
├── utils/
├── App.tsx
├── main.tsx
└── App.module.css # CSS modules
```
## VS Code Setup
Install recommended extensions:
1. **PostCSS Intellisense and Highlighting**
For postcss syntax support and `$variable` recognition.
2. **CSS Variable Autocomplete**
For Mantine CSS variables autocomplete.
Create `.vscode/settings.json`:
```json
{
"cssVariables.lookupFiles": [
"**/*.css",
"node_modules/@mantine/core/styles.css"
]
}
```
## TypeScript Configuration
Mantine is fully typed. Your `tsconfig.json` should have:
```json
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"jsx": "react-jsx",
"skipLibCheck": true
}
}
```
## Common Issues
### Styles Not Loading
Ensure `@mantine/core/styles.css` is imported before any component usage.
### PostCSS Mixins Not Working
Check that `postcss-preset-mantine` is installed and configured in `postcss.config.cjs`.
### Dark Mode Flash
For SSR apps, add `ColorSchemeScript` to `<head>`:
```tsx
import { ColorSchemeScript } from '@mantine/core';
// In your HTML head
<ColorSchemeScript defaultColorScheme="auto" />
```
### Hydration Warning
Spread `mantineHtmlProps` on `<html>` element:
```tsx
import { mantineHtmlProps } from '@mantine/core';
<html {...mantineHtmlProps}>
```

View File

@@ -0,0 +1,344 @@
# Hooks Reference
`@mantine/hooks` provides 75+ React hooks. No styles needed — works independently.
```bash
npm install @mantine/hooks
```
## State Management
### useDisclosure
Boolean state for modals/menus:
```tsx
import { useDisclosure } from '@mantine/hooks';
const [opened, { open, close, toggle }] = useDisclosure(false);
// With callbacks
const [opened, handlers] = useDisclosure(false, {
onOpen: () => console.log('Opened'),
onClose: () => console.log('Closed'),
});
```
### useToggle
Cycle through values:
```tsx
import { useToggle } from '@mantine/hooks';
const [value, toggle] = useToggle(['blue', 'orange', 'cyan']);
// toggle() cycles, toggle('cyan') sets specific
// Boolean
const [active, toggle] = useToggle([false, true]);
```
### useCounter
Numeric counter with min/max:
```tsx
import { useCounter } from '@mantine/hooks';
const [count, { increment, decrement, set, reset }] = useCounter(0, { min: 0, max: 10 });
```
### useListState
Array state management:
```tsx
import { useListState } from '@mantine/hooks';
const [values, handlers] = useListState([1, 2, 3]);
handlers.append(4); // Add to end
handlers.prepend(0); // Add to start
handlers.insert(2, 99); // Insert at index
handlers.remove(1); // Remove at index
handlers.reorder({ from: 0, to: 2 }); // Move item
handlers.filter((item) => item > 2); // Filter
handlers.apply((item) => item * 2); // Transform all
```
### useSetState
Object state with partial updates:
```tsx
import { useSetState } from '@mantine/hooks';
const [state, setState] = useSetState({ name: '', email: '', age: 0 });
setState({ name: 'John' }); // Partial update, others unchanged
```
## Storage
### useLocalStorage / useSessionStorage
```tsx
import { useLocalStorage } from '@mantine/hooks';
const [value, setValue, removeValue] = useLocalStorage({
key: 'my-key',
defaultValue: 'light',
});
// Auto-serializes objects
const [user, setUser] = useLocalStorage({
key: 'user',
defaultValue: { name: '', preferences: {} },
});
```
## Input/Debounce
### useDebouncedValue
```tsx
import { useDebouncedValue } from '@mantine/hooks';
const [value, setValue] = useState('');
const [debounced] = useDebouncedValue(value, 300);
useEffect(() => {
// API call with debounced value
}, [debounced]);
```
### useDebouncedCallback
```tsx
import { useDebouncedCallback } from '@mantine/hooks';
const search = useDebouncedCallback(async (query: string) => {
await searchAPI(query);
}, 300);
<TextInput onChange={(e) => search(e.target.value)} />
```
### useInputState
Simpler input handling:
```tsx
import { useInputState } from '@mantine/hooks';
const [name, setName] = useInputState('');
<TextInput value={name} onChange={setName} />
const [checked, setChecked] = useInputState(false);
<Checkbox checked={checked} onChange={setChecked} />
```
## UI Interactions
### useClickOutside
```tsx
import { useClickOutside } from '@mantine/hooks';
const ref = useClickOutside(() => close());
<Paper ref={ref}>Click outside to close</Paper>
```
### useHover
```tsx
import { useHover } from '@mantine/hooks';
const { hovered, ref } = useHover();
<Box ref={ref} bg={hovered ? 'blue' : 'gray'}>Hover me</Box>
```
### useFocusWithin
```tsx
import { useFocusWithin } from '@mantine/hooks';
const { ref, focused } = useFocusWithin();
<Box ref={ref} style={{ outline: focused ? '2px solid blue' : 'none' }}>
<TextInput /><TextInput />
</Box>
```
### useMediaQuery
```tsx
import { useMediaQuery } from '@mantine/hooks';
const isMobile = useMediaQuery('(max-width: 768px)');
const matches = useMediaQuery('(min-width: 48em)'); // sm breakpoint
return isMobile ? <MobileNav /> : <DesktopNav />;
```
### useViewportSize
```tsx
import { useViewportSize } from '@mantine/hooks';
const { width, height } = useViewportSize();
```
### useElementSize
```tsx
import { useElementSize } from '@mantine/hooks';
const { ref, width, height } = useElementSize();
<Box ref={ref}>Tracks this element's size</Box>
```
### useScrollIntoView
```tsx
import { useScrollIntoView } from '@mantine/hooks';
const { scrollIntoView, targetRef } = useScrollIntoView<HTMLDivElement>({
offset: 60,
duration: 500,
});
<Button onClick={() => scrollIntoView()}>Scroll</Button>
<div ref={targetRef}>Target</div>
```
### useIntersection / useInViewport
```tsx
import { useIntersection, useInViewport } from '@mantine/hooks';
// Detailed intersection info
const { ref, entry } = useIntersection({ threshold: 0.5 });
// Simple visibility check
const { ref, inViewport } = useInViewport();
```
## Utilities
### useClipboard
```tsx
import { useClipboard } from '@mantine/hooks';
const clipboard = useClipboard({ timeout: 500 });
<Button onClick={() => clipboard.copy('Text')} color={clipboard.copied ? 'teal' : 'blue'}>
{clipboard.copied ? 'Copied!' : 'Copy'}
</Button>
```
### useHotkeys
```tsx
import { useHotkeys, getHotkeyHandler } from '@mantine/hooks';
useHotkeys([
['mod+S', () => save()],
['ctrl+K', () => search()],
]);
// On specific input
<input onKeyDown={getHotkeyHandler([
['mod+Enter', submit],
['Escape', cancel],
])} />
```
### useFullscreen
```tsx
import { useFullscreen } from '@mantine/hooks';
const { toggle, fullscreen } = useFullscreen();
<Button onClick={toggle}>{fullscreen ? 'Exit' : 'Enter'} Fullscreen</Button>
```
### useIdle
```tsx
import { useIdle } from '@mantine/hooks';
const idle = useIdle(5000); // 5 seconds
<Text>{idle ? 'User is idle' : 'User is active'}</Text>
```
### useInterval / useTimeout
```tsx
import { useInterval, useTimeout } from '@mantine/hooks';
const interval = useInterval(() => tick(), 1000);
interval.start(); interval.stop(); interval.toggle();
const { start, clear } = useTimeout(() => action(), 3000);
```
### useDocumentTitle
```tsx
import { useDocumentTitle } from '@mantine/hooks';
useDocumentTitle('My Page Title');
```
### useOs
```tsx
import { useOs } from '@mantine/hooks';
const os = useOs(); // 'macos' | 'ios' | 'windows' | 'android' | 'linux'
<Text>Shortcut: {os === 'macos' ? '⌘' : 'Ctrl'}</Text>
```
### useNetwork
```tsx
import { useNetwork } from '@mantine/hooks';
const { online, downlink, effectiveType } = useNetwork();
return online ? <App /> : <OfflineMessage />;
```
### usePrevious
```tsx
import { usePrevious } from '@mantine/hooks';
const [value, setValue] = useState(0);
const previous = usePrevious(value);
```
### useMergedRef
Combine multiple refs:
```tsx
import { useMergedRef } from '@mantine/hooks';
const myRef = useRef<HTMLDivElement>(null);
const { ref: hookRef } = useHover();
const mergedRef = useMergedRef(myRef, hookRef);
<Box ref={mergedRef}>Content</Box>
```
## Complete Hook List
**State:** useDisclosure, useToggle, useCounter, useListState, useSetState, useQueue, usePagination
**Storage:** useLocalStorage, useSessionStorage, readLocalStorageValue
**Input:** useDebouncedValue, useDebouncedState, useDebouncedCallback, useInputState, useValidatedState, useUncontrolled
**UI:** useClickOutside, useHover, useFocusWithin, useFocusReturn, useFocusTrap, useMediaQuery, useViewportSize, useElementSize, useResizeObserver, useScrollIntoView, useIntersection, useInViewport, useWindowScroll, useMouse, useMove
**Utilities:** useClipboard, useHotkeys, useFullscreen, useIdle, useInterval, useTimeout, useDocumentTitle, useDocumentVisibility, useFavicon, useOs, useNetwork, usePrevious, useMergedRef, useId, useForceUpdate, useReducedMotion, useTextSelection, useWindowEvent, useEventListener, useEyeDropper, useHash, useHeadroom, useLogger, useMutationObserver, useOrientation, usePageLeave, usePinchToZoom, useStateHistory

View File

@@ -0,0 +1,472 @@
# Styling & Theming Reference
Mantine styling: MantineProvider, theme object, CSS modules, style props, and Styles API.
## MantineProvider
Required wrapper for all Mantine components:
```tsx
import { createTheme, MantineProvider } from '@mantine/core';
const theme = createTheme({
primaryColor: 'blue',
fontFamily: 'Inter, sans-serif',
});
function App() {
return (
<MantineProvider theme={theme}>
{/* App content */}
</MantineProvider>
);
}
```
### Key Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `theme` | `MantineThemeOverride` | - | Theme customization |
| `defaultColorScheme` | `'light' \| 'dark' \| 'auto'` | `'light'` | Default color scheme |
| `forceColorScheme` | `'light' \| 'dark'` | - | Force specific scheme |
| `env` | `'default' \| 'test'` | `'default'` | Disable transitions for tests |
## Theme Object
```tsx
import { createTheme, rem } from '@mantine/core';
const theme = createTheme({
// Colors
primaryColor: 'blue',
primaryShade: { light: 6, dark: 8 },
// Typography
fontFamily: 'Inter, sans-serif',
headings: {
fontFamily: 'Greycliff CF, sans-serif',
fontWeight: '700',
},
// Spacing & Sizing
spacing: { xs: rem(10), sm: rem(12), md: rem(16), lg: rem(20), xl: rem(32) },
radius: { xs: rem(2), sm: rem(4), md: rem(8), lg: rem(16), xl: rem(32) },
// Defaults
defaultRadius: 'md',
cursorType: 'pointer',
respectReducedMotion: true,
});
```
## Custom Colors
```tsx
import { createTheme, MantineColorsTuple } from '@mantine/core';
const brand: MantineColorsTuple = [
'#f0f9ff', '#e0f2fe', '#bae6fd', '#7dd3fc', '#38bdf8',
'#0ea5e9', '#0284c7', '#0369a1', '#075985', '#0c4a6e',
];
const theme = createTheme({
colors: { brand },
primaryColor: 'brand',
});
```
## Color Scheme (Dark Mode)
```tsx
import { useMantineColorScheme, useComputedColorScheme } from '@mantine/core';
function ColorSchemeToggle() {
const { setColorScheme, toggleColorScheme } = useMantineColorScheme();
const computed = useComputedColorScheme('light'); // Resolved value
return (
<>
<Button onClick={() => setColorScheme('light')}>Light</Button>
<Button onClick={() => setColorScheme('dark')}>Dark</Button>
<Button onClick={toggleColorScheme}>Toggle</Button>
</>
);
}
// SSR: Prevent flash of wrong color scheme
import { ColorSchemeScript, mantineHtmlProps } from '@mantine/core';
<html {...mantineHtmlProps}>
<head>
<ColorSchemeScript defaultColorScheme="auto" />
</head>
</html>
```
## Style Props
All components accept style props for quick styling:
```tsx
import { Box, Text, Button } from '@mantine/core';
function Demo() {
return (
<Box
p="md" // padding
m="lg" // margin
mt="xl" // margin-top
bg="blue.6" // background (color.shade)
c="white" // color
w={200} // width (number = px)
h="100%" // height
maw={500} // max-width
pos="relative" // position
ta="center" // text-align
fz="sm" // font-size
fw={700} // font-weight
ff="monospace" // font-family
lh={1.5} // line-height
style={{ borderRadius: 'var(--mantine-radius-md)' }}
>
<Text c="dimmed" fz="xs" tt="uppercase">
Uppercase dimmed text
</Text>
</Box>
);
}
```
### Common Style Props
| Prop | CSS Property | Example |
|------|--------------|---------|
| `m`, `mx`, `my`, `mt`, `mr`, `mb`, `ml` | margin | `m="md"`, `mt={20}` |
| `p`, `px`, `py`, `pt`, `pr`, `pb`, `pl` | padding | `p="lg"` |
| `w`, `h`, `maw`, `mah`, `miw`, `mih` | width, height, max/min | `w="100%"` |
| `c` | color | `c="blue.6"`, `c="dimmed"` |
| `bg` | background-color | `bg="gray.1"` |
| `fz` | font-size | `fz="sm"`, `fz={14}` |
| `fw` | font-weight | `fw={500}`, `fw="bold"` |
| `ta` | text-align | `ta="center"` |
| `td` | text-decoration | `td="underline"` |
| `tt` | text-transform | `tt="uppercase"` |
| `ff` | font-family | `ff="monospace"` |
| `lh` | line-height | `lh={1.5}` |
| `pos` | position | `pos="absolute"` |
| `top`, `left`, `right`, `bottom` | position offsets | `top={10}` |
| `display` | display | `display="flex"` |
| `opacity` | opacity | `opacity={0.5}` |
### Responsive Props
```tsx
<Box
w={{ base: '100%', sm: '50%', md: 400 }}
p={{ base: 'xs', md: 'xl' }}
display={{ base: 'none', md: 'block' }}
>
Responsive box
</Box>
```
## CSS Modules
Recommended styling approach. Create `.module.css` files:
```css
/* Button.module.css */
.root {
background-color: var(--mantine-color-blue-6);
@mixin hover {
background-color: var(--mantine-color-blue-7);
}
/* Responsive */
@mixin smaller-than sm {
font-size: var(--mantine-font-size-xs);
}
@mixin larger-than md {
padding: var(--mantine-spacing-xl);
}
/* Dark mode */
@mixin dark {
background-color: var(--mantine-color-blue-8);
}
@mixin light {
background-color: var(--mantine-color-blue-4);
}
}
.label {
color: var(--mantine-color-white);
}
```
```tsx
import { Button } from '@mantine/core';
import classes from './Button.module.css';
function Demo() {
return (
<Button classNames={classes}>
Styled button
</Button>
);
}
```
## PostCSS Preset
`postcss-preset-mantine` provides:
### Mixins
```css
/* Hover state */
@mixin hover {
/* Hover-only styles */
}
/* Responsive breakpoints */
@mixin smaller-than sm { }
@mixin larger-than md { }
/* Color scheme */
@mixin light { }
@mixin dark { }
/* RTL support */
@mixin rtl { }
@mixin ltr { }
```
### Functions
```css
.element {
/* rem() - convert to rem */
font-size: rem(16px); /* 1rem */
/* em() - convert to em */
padding: em(24px); /* 1.5em */
/* light-dark() - color scheme values */
background: light-dark(white, black);
/* alpha() - add opacity to color */
background: alpha(var(--mantine-color-blue-5), 0.5);
}
```
## Styles API
Override internal component styles:
### classNames Prop
```tsx
import { TextInput } from '@mantine/core';
import classes from './TextInput.module.css';
// CSS module with selectors matching Styles API
// .root, .input, .label, .error, etc.
<TextInput
classNames={{
root: classes.root,
input: classes.input,
label: classes.label,
}}
/>
```
### styles Prop (CSS-in-JS)
```tsx
<TextInput
styles={{
root: { marginBottom: 20 },
input: { backgroundColor: 'var(--mantine-color-gray-0)' },
label: { fontWeight: 700 },
}}
/>
```
### styles Function
```tsx
<TextInput
styles={(theme, props) => ({
input: {
borderColor: props.error
? theme.colors.red[6]
: theme.colors.gray[4],
},
})}
/>
```
### Finding Selectors
All Styles API selectors are documented for each component. Common patterns:
- `root` - Root element
- `label` - Label text
- `input` - Input element
- `wrapper` - Input wrapper
- `error` - Error message
- `description` - Description text
- `required` - Required asterisk
- `section` - Input sections (left/right icons)
## hiddenFrom / visibleFrom
Hide/show at breakpoints:
```tsx
import { Text } from '@mantine/core';
<Text hiddenFrom="sm">Hidden on sm and larger</Text>
<Text visibleFrom="md">Visible only on md and larger</Text>
```
## lightHidden / darkHidden
Hide based on color scheme:
```tsx
<Text lightHidden>Only in dark mode</Text>
<Text darkHidden>Only in light mode</Text>
```
## Box Component
Base component for custom styling:
```tsx
import { Box } from '@mantine/core';
<Box
component="section" // Render as different element
className={classes.wrapper}
p="md"
bg="gray.1"
style={{ borderRadius: 'var(--mantine-radius-md)' }}
>
Content
</Box>
```
## Polymorphic Components
Many components accept `component` prop:
```tsx
import { Button } from '@mantine/core';
import { Link } from 'react-router-dom';
// Render Button as Link
<Button component={Link} to="/about">
About
</Button>
// Render as native anchor
<Button component="a" href="https://example.com">
External
</Button>
```
## CSS Variables in Styles
Access theme values:
```tsx
<Box
style={{
backgroundColor: 'var(--mantine-color-blue-6)',
padding: 'var(--mantine-spacing-md)',
borderRadius: 'var(--mantine-radius-sm)',
boxShadow: 'var(--mantine-shadow-md)',
}}
>
Styled with CSS variables
</Box>
```
## Global Styles
```tsx
// In your CSS
:root {
--my-custom-color: #ff6b6b;
}
/* Target Mantine root element */
[data-mantine-color-scheme="dark"] {
--my-custom-color: #ff8787;
}
/* Global component overrides */
.mantine-Button-root {
font-weight: 600;
}
```
## rem() and em() Utilities
```tsx
import { rem, em } from '@mantine/core';
// In styles or inline
<Box style={{ fontSize: rem(16), padding: rem(24) }} />
// rem(16) => '1rem'
// em(24) => '1.5em'
```
## Style Props vs CSS Modules
| Use Case | Recommended |
|----------|-------------|
| Quick prototyping | Style props |
| Simple spacing/colors | Style props |
| Complex hover/focus states | CSS modules |
| Responsive layouts | CSS modules |
| Reusable component styles | CSS modules |
| Performance critical | CSS modules |
## Component Default Props (Theme)
Override defaults globally:
```tsx
const theme = createTheme({
components: {
Button: Button.extend({
defaultProps: { variant: 'outline', size: 'md', radius: 'xl' },
}),
TextInput: TextInput.extend({
defaultProps: { size: 'md' },
classNames: { root: 'my-input-root', input: 'my-input' },
}),
},
});
```
## CSS Variables Reference
```
--mantine-color-{color}-{shade} // Colors (0-9)
--mantine-primary-color-{shade} // Primary color
--mantine-spacing-{size} // xs, sm, md, lg, xl
--mantine-radius-{size} // xs, sm, md, lg, xl
--mantine-font-family // Main font
--mantine-font-size-{size} // xs, sm, md, lg, xl
--mantine-breakpoint-{size} // Responsive breakpoints
```

View File

@@ -0,0 +1,395 @@
# Testing Reference
Guide for testing Mantine applications with Vitest and React Testing Library.
## Installation
```bash
npm install -D vitest jsdom @testing-library/dom @testing-library/jest-dom @testing-library/react @testing-library/user-event
```
## Vitest Configuration
Add to `vite.config.ts`:
```ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './vitest.setup.mjs',
},
});
```
## Setup File
Create `vitest.setup.mjs`:
```js
import '@testing-library/jest-dom/vitest';
import { vi } from 'vitest';
// Fix for getComputedStyle
const { getComputedStyle } = window;
window.getComputedStyle = (elt) => getComputedStyle(elt);
// Mock scrollIntoView
window.HTMLElement.prototype.scrollIntoView = () => {};
// Mock matchMedia (required by Mantine)
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
// Mock ResizeObserver (required by some Mantine components)
class ResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
}
window.ResizeObserver = ResizeObserver;
```
## Custom Render Function
All Mantine components require MantineProvider. Create custom render:
```tsx
// test-utils/render.tsx
import { render as testingLibraryRender } from '@testing-library/react';
import { MantineProvider } from '@mantine/core';
import { theme } from '../src/theme'; // Your theme if any
export function render(ui: React.ReactNode) {
return testingLibraryRender(<>{ui}</>, {
wrapper: ({ children }) => (
<MantineProvider theme={theme} env="test">
{children}
</MantineProvider>
),
});
}
```
### Important: env="test"
Setting `env="test"` on MantineProvider:
- Disables CSS transitions (tests run faster)
- Disables portals (elements render in place, easier to query)
## Export Test Utilities
```tsx
// test-utils/index.ts
import userEvent from '@testing-library/user-event';
export * from '@testing-library/react';
export { render } from './render';
export { userEvent };
```
## Writing Tests
### Basic Component Test
```tsx
// Button.test.tsx
import { render, screen } from '../test-utils';
import { Button } from '@mantine/core';
describe('Button', () => {
it('renders children', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument();
});
it('handles click events', async () => {
const onClick = vi.fn();
render(<Button onClick={onClick}>Click</Button>);
await userEvent.click(screen.getByRole('button'));
expect(onClick).toHaveBeenCalledTimes(1);
});
});
```
### Testing Form Inputs
```tsx
import { render, screen } from '../test-utils';
import { userEvent } from '../test-utils';
import { TextInput } from '@mantine/core';
describe('TextInput', () => {
it('accepts user input', async () => {
const onChange = vi.fn();
render(<TextInput onChange={onChange} label="Name" />);
const input = screen.getByLabelText('Name');
await userEvent.type(input, 'John Doe');
expect(input).toHaveValue('John Doe');
expect(onChange).toHaveBeenCalled();
});
it('displays error', () => {
render(<TextInput label="Email" error="Invalid email" />);
expect(screen.getByText('Invalid email')).toBeInTheDocument();
});
});
```
### Testing useForm
```tsx
import { render, screen, waitFor } from '../test-utils';
import { userEvent } from '../test-utils';
import { useForm } from '@mantine/form';
import { TextInput, Button } from '@mantine/core';
function TestForm() {
const form = useForm({
mode: 'uncontrolled',
initialValues: { email: '' },
validate: {
email: (v) => (!v.includes('@') ? 'Invalid email' : null),
},
});
return (
<form onSubmit={form.onSubmit((values) => console.log(values))}>
<TextInput
label="Email"
key={form.key('email')}
{...form.getInputProps('email')}
/>
<Button type="submit">Submit</Button>
</form>
);
}
describe('Form', () => {
it('validates on submit', async () => {
render(<TestForm />);
await userEvent.click(screen.getByRole('button', { name: /submit/i }));
expect(screen.getByText('Invalid email')).toBeInTheDocument();
});
it('clears error on valid input', async () => {
render(<TestForm />);
await userEvent.type(screen.getByLabelText('Email'), 'test@email.com');
await userEvent.click(screen.getByRole('button', { name: /submit/i }));
expect(screen.queryByText('Invalid email')).not.toBeInTheDocument();
});
});
```
### Testing Modals
With `env="test"`, modals render in place (no portal):
```tsx
import { render, screen } from '../test-utils';
import { userEvent } from '../test-utils';
import { useDisclosure } from '@mantine/hooks';
import { Modal, Button } from '@mantine/core';
function ModalDemo() {
const [opened, { open, close }] = useDisclosure(false);
return (
<>
<Button onClick={open}>Open</Button>
<Modal opened={opened} onClose={close} title="Test Modal">
Modal Content
</Modal>
</>
);
}
describe('Modal', () => {
it('opens when button is clicked', async () => {
render(<ModalDemo />);
expect(screen.queryByText('Modal Content')).not.toBeInTheDocument();
await userEvent.click(screen.getByRole('button', { name: /open/i }));
expect(screen.getByText('Modal Content')).toBeInTheDocument();
});
});
```
### Testing Select/Dropdown
```tsx
import { render, screen } from '../test-utils';
import { userEvent } from '../test-utils';
import { Select } from '@mantine/core';
describe('Select', () => {
it('selects an option', async () => {
const onChange = vi.fn();
render(
<Select
label="Country"
data={['USA', 'Canada', 'UK']}
onChange={onChange}
/>
);
// Open dropdown
await userEvent.click(screen.getByLabelText('Country'));
// Select option
await userEvent.click(screen.getByRole('option', { name: 'Canada' }));
expect(onChange).toHaveBeenCalledWith('Canada');
});
});
```
### Testing Color Scheme
```tsx
import { render, screen } from '../test-utils';
import { useMantineColorScheme } from '@mantine/core';
function ColorSchemeToggle() {
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
return (
<button onClick={toggleColorScheme}>
Current: {colorScheme}
</button>
);
}
describe('Color Scheme', () => {
it('toggles color scheme', async () => {
render(<ColorSchemeToggle />);
expect(screen.getByText(/current: light/i)).toBeInTheDocument();
await userEvent.click(screen.getByRole('button'));
expect(screen.getByText(/current: dark/i)).toBeInTheDocument();
});
});
```
### Testing Notifications
```tsx
import { render, screen, waitFor } from '../test-utils';
import { notifications } from '@mantine/notifications';
import { Notifications } from '@mantine/notifications';
import { Button } from '@mantine/core';
function NotificationDemo() {
return (
<>
<Notifications />
<Button onClick={() => notifications.show({ message: 'Hello!' })}>
Show Notification
</Button>
</>
);
}
describe('Notifications', () => {
it('shows notification', async () => {
render(<NotificationDemo />);
await userEvent.click(screen.getByRole('button'));
await waitFor(() => {
expect(screen.getByText('Hello!')).toBeInTheDocument();
});
});
});
```
## Testing Hooks
```tsx
import { renderHook, act } from '@testing-library/react';
import { useDisclosure } from '@mantine/hooks';
describe('useDisclosure', () => {
it('toggles state', () => {
const { result } = renderHook(() => useDisclosure(false));
expect(result.current[0]).toBe(false);
act(() => {
result.current[1].open();
});
expect(result.current[0]).toBe(true);
act(() => {
result.current[1].close();
});
expect(result.current[0]).toBe(false);
});
});
```
## Scripts
Add to `package.json`:
```json
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
}
}
```
## Common Issues
### matchMedia Error
Ensure `matchMedia` mock is in setup file.
### ResizeObserver Error
Ensure `ResizeObserver` mock is in setup file.
### Portal Elements Not Found
Use `env="test"` on MantineProvider to disable portals.
### Transitions Cause Timing Issues
Use `env="test"` to disable transitions.
### Elements Not in Document
Make sure to use custom `render` that includes MantineProvider.
## Testing Checklist
- [ ] Setup file mocks matchMedia and ResizeObserver
- [ ] Custom render includes MantineProvider with `env="test"`
- [ ] Use `userEvent` for user interactions (not `fireEvent`)
- [ ] Use `waitFor` for async operations
- [ ] Use `getByRole` with accessible names when possible
- [ ] Test error states and edge cases

View File

@@ -0,0 +1,186 @@
---
name: prisma-database-setup
description: Guides for configuring Prisma with different database providers (PostgreSQL, MySQL, SQLite, MongoDB, etc.). Use when setting up a new project, changing databases, or troubleshooting connection issues. Triggers on "configure postgres", "connect to mysql", "setup mongodb", "sqlite setup".
license: MIT
metadata:
author: prisma
version: "1.0.0"
---
# Prisma Database Setup
Comprehensive guides for configuring Prisma ORM with various database providers.
## When to Apply
Reference this skill when:
- Initializing a new Prisma project
- Switching database providers
- Configuring connection strings and environment variables
- Troubleshooting database connection issues
- Setting up database-specific features
- Generating and instantiating Prisma Client
## Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|----------|----------|--------|--------|
| 1 | Provider Guides | CRITICAL | provider names |
| 2 | Prisma Postgres | HIGH | `prisma-postgres` |
| 3 | Client Setup | CRITICAL | `prisma-client-setup` |
## System Prerequisites (Prisma ORM 7)
- **Node.js 20.19.0+**
- **TypeScript 5.4.0+**
## Bun Runtime
If you're using Bun, run Prisma CLI commands with `bunx --bun prisma ...` so Prisma uses the Bun runtime instead of falling back to Node.js.
## Supported Databases
| Database | Provider String | Notes |
|----------|-----------------|-------|
| PostgreSQL | `postgresql` | Default, full feature support |
| MySQL | `mysql` | Widespread support, some JSON diffs |
| SQLite | `sqlite` | Local file-based, no enum/scalar lists |
| MongoDB | `mongodb` | **NOT SUPPORTED IN v7** (Use v6) |
| SQL Server | `sqlserver` | Microsoft ecosystem |
| CockroachDB | `cockroachdb` | Distributed SQL, Postgres-compatible |
| Prisma Postgres | `postgresql` | Managed serverless database |
## Configuration Files
Prisma v7 uses two main files for configuration:
1. **`prisma/schema.prisma`**: Defines the `datasource` block.
2. **`prisma.config.ts`**: Configures the connection URL (replaces env loading in schema).
## Driver Adapters (Prisma ORM 7)
Prisma ORM 7 uses the query compiler by default, which **requires a driver adapter**. Choose the adapter and driver for your database and pass the adapter to `PrismaClient`.
| Database | Adapter | JS Driver |
|----------|---------|-----------|
| PostgreSQL | `@prisma/adapter-pg` | `pg` |
| CockroachDB | `@prisma/adapter-pg` | `pg` |
| Prisma Postgres | `@prisma/adapter-ppg` | `@prisma/ppg` |
| MySQL / MariaDB | `@prisma/adapter-mariadb` | `mariadb` |
| SQLite | `@prisma/adapter-better-sqlite3` | `better-sqlite3` |
| SQLite (Turso/LibSQL) | `@prisma/adapter-libsql` | `@libsql/client` |
| SQL Server | `@prisma/adapter-mssql` | `node-mssql` |
Example (PostgreSQL):
```ts
import 'dotenv/config'
import { PrismaClient } from '../generated/client'
import { PrismaPg } from '@prisma/adapter-pg'
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL })
const prisma = new PrismaClient({ adapter })
```
## Prisma Client Setup (Required)
Prisma Client must be installed and generated for any database.
1. Install Prisma CLI and Prisma Client:
```bash
npm install prisma --save-dev
npm install @prisma/client
```
1. Add a generator block (output is required in Prisma v7):
```prisma
generator client {
provider = "prisma-client"
output = "../generated"
}
```
1. Generate Prisma Client:
```bash
npx prisma generate
```
1. Instantiate Prisma Client with the database-specific driver adapter:
```typescript
import { PrismaClient } from '../generated/client'
import { PrismaPg } from '@prisma/adapter-pg'
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL })
const prisma = new PrismaClient({ adapter })
```
1. Re-run `prisma generate` after every schema change.
## Quick Reference
### PostgreSQL
```prisma
datasource db {
provider = "postgresql"
}
generator client {
provider = "prisma-client"
output = "../generated"
}
```
### MySQL
```prisma
datasource db {
provider = "mysql"
}
generator client {
provider = "prisma-client"
output = "../generated"
}
```
### SQLite
```prisma
datasource db {
provider = "sqlite"
}
generator client {
provider = "prisma-client"
output = "../generated"
}
```
### MongoDB (Prisma v6 only)
```prisma
datasource db {
provider = "mongodb"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
```
## Rule Files
See individual rule files for detailed setup instructions:
```
references/postgresql.md
references/mysql.md
references/sqlite.md
references/mongodb.md
references/sqlserver.md
references/cockroachdb.md
references/prisma-postgres.md
references/prisma-client-setup.md
```
## How to Use
Choose the provider reference file for your database, then apply `references/prisma-client-setup.md` to complete client generation and adapter setup.

View File

@@ -0,0 +1,89 @@
# CockroachDB Setup
Configure Prisma with CockroachDB.
## Prerequisites
- CockroachDB cluster
## 1. Schema Configuration
In `prisma/schema.prisma`:
```prisma
datasource db {
provider = "cockroachdb"
}
generator client {
provider = "prisma-client"
output = "../generated"
}
```
## 2. Config Configuration (v7)
In `prisma.config.ts`:
```typescript
import { defineConfig, env } from 'prisma/config'
export default defineConfig({
schema: 'prisma/schema.prisma',
datasource: {
url: env('DATABASE_URL'),
},
})
```
## 3. Environment Variable
In `.env`:
```env
DATABASE_URL="postgresql://user:password@host:26257/db?sslmode=verify-full"
```
Note: CockroachDB uses the PostgreSQL wire protocol, so the URL often looks like postgresql, but the provider **MUST** be `cockroachdb` in the schema to handle specific CRDB features correctly.
## Driver Adapter (Prisma ORM 7 required)
Prisma ORM 7 uses the query compiler by default, so you must use a driver adapter. CockroachDB is PostgreSQL-compatible, so use the PostgreSQL adapter.
1. Install adapter and driver:
```bash
npm install @prisma/adapter-pg pg
```
2. Instantiate Prisma Client with the adapter:
```typescript
import 'dotenv/config'
import { PrismaClient } from '../generated/client'
import { PrismaPg } from '@prisma/adapter-pg'
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL })
const prisma = new PrismaClient({ adapter })
```
## ID Generation
CockroachDB uses `BigInt` or `UUID` for IDs efficiently.
```prisma
model User {
id BigInt @id @default(autoincrement()) // Uses unique_rowid()
}
```
Or using string UUIDs:
```prisma
model User {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
}
```
## Common Issues
### Schema Introspection
Always use `provider = "cockroachdb"` to ensure correct type mapping during `db pull`.

View File

@@ -0,0 +1,72 @@
# MongoDB Setup
**⚠️ WARNING: MongoDB is NOT supported in Prisma ORM v7.**
Support for MongoDB is planned for a future v7 release. If you need MongoDB, you must use **Prisma ORM v6**.
## Prerequisites
- MongoDB 4.2+
- Replica Set configured (required for transactions)
- **Prisma ORM v6.x**
## 1. Schema Configuration (v6)
In `prisma/schema.prisma`:
```prisma
datasource db {
provider = "mongodb"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
```
### ID Field Requirement
MongoDB models **must** have a mapped `_id` field using `@id` and `@map("_id")`, usually of type `String` with `auto()` and `db.ObjectId`.
```prisma
model User {
id String @id @default(auto()) @map("_id") @db.ObjectId
email String @unique
name String?
}
```
### Relations
Relations in MongoDB expect IDs to be `db.ObjectId` type.
```prisma
model Post {
id String @id @default(auto()) @map("_id") @db.ObjectId
author User @relation(fields: [authorId], references: [id])
authorId String @db.ObjectId
}
```
## 2. Environment Variable
In `.env`:
```env
DATABASE_URL="mongodb+srv://user:password@cluster.mongodb.net/mydb?retryWrites=true&w=majority"
```
## Migrations vs Introspection
- **No Migrations**: MongoDB is schema-less. `prisma migrate` commands **do not work**.
- **db push**: Use `prisma db push` to sync indexes and constraints.
- **db pull**: Use `prisma db pull` to generate schema from existing data (sampling).
## Common Issues
### "Transactions not supported"
Ensure your MongoDB instance is a **Replica Set**. Standalone instances do not support transactions. Atlas clusters are replica sets by default.
### "Invalid ObjectID"
Ensure fields referencing IDs are decorated with `@db.ObjectId` if the target is an ObjectID.

View File

@@ -0,0 +1,109 @@
# MySQL Setup
Configure Prisma with MySQL (or MariaDB).
## Prerequisites
- MySQL or MariaDB database
- Connection string
## 1. Schema Configuration
In `prisma/schema.prisma`:
```prisma
datasource db {
provider = "mysql"
}
generator client {
provider = "prisma-client"
output = "../generated"
}
```
## 2. Config Configuration (v7)
In `prisma.config.ts`:
```typescript
import { defineConfig, env } from 'prisma/config'
export default defineConfig({
schema: 'prisma/schema.prisma',
datasource: {
url: env('DATABASE_URL'),
},
})
```
## 3. Environment Variable
In `.env`:
```env
DATABASE_URL="mysql://user:password@localhost:3306/mydb"
```
### Connection String Format
```
mysql://USER:PASSWORD@HOST:PORT/DATABASE
```
- **USER**: Database user
- **PASSWORD**: Password
- **HOST**: Hostname
- **PORT**: Port (default 3306)
- **DATABASE**: Database name
## Driver Adapter (Prisma ORM 7 required)
Prisma ORM 7 uses the query compiler by default, so you must use a driver adapter.
1. Install adapter and driver:
```bash
npm install @prisma/adapter-mariadb mariadb
```
2. Instantiate Prisma Client with the adapter:
```typescript
import 'dotenv/config'
import { PrismaClient } from '../generated/client'
import { PrismaMariaDb } from '@prisma/adapter-mariadb'
const adapter = new PrismaMariaDb({
host: 'localhost',
port: 3306,
connectionLimit: 5,
user: process.env.MYSQL_USER,
password: process.env.MYSQL_PASSWORD,
database: process.env.MYSQL_DATABASE,
})
const prisma = new PrismaClient({ adapter })
```
## PlanetScale Setup
PlanetScale uses MySQL but requires specific settings because it doesn't support foreign key constraints.
In `prisma/schema.prisma`:
```prisma
datasource db {
provider = "mysql"
relationMode = "prisma" // Emulate foreign keys in Prisma
}
```
## Common Issues
### "Too many connections"
MySQL has a connection limit. Adjust connection pool size in URL:
```env
DATABASE_URL="mysql://...?connection_limit=5"
```
### JSON Support
MySQL 5.7+ supports JSON. MariaDB 10.2+ supports JSON (as an alias for LONGTEXT with check constraints). Prisma handles this, but verify your version.

View File

@@ -0,0 +1,92 @@
# PostgreSQL Setup
Configure Prisma with PostgreSQL.
## Prerequisites
- PostgreSQL database (local or cloud)
- Connection string
## 1. Schema Configuration
In `prisma/schema.prisma`:
```prisma
datasource db {
provider = "postgresql"
}
generator client {
provider = "prisma-client"
output = "../generated"
}
```
## 2. Config Configuration (v7)
In `prisma.config.ts`:
```typescript
import { defineConfig, env } from 'prisma/config'
export default defineConfig({
schema: 'prisma/schema.prisma',
datasource: {
url: env('DATABASE_URL'),
},
})
```
## 3. Environment Variable
In `.env`:
```env
DATABASE_URL="postgresql://user:password@localhost:5432/mydb?schema=public"
```
### Connection String Format
```
postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=SCHEMA
```
- **USER**: Database user
- **PASSWORD**: Password (URL encoded if special chars)
- **HOST**: Hostname (localhost, IP, or domain)
- **PORT**: Port (default 5432)
- **DATABASE**: Database name
- **SCHEMA**: Schema name (default `public`)
## Driver Adapter (Prisma ORM 7 required)
Prisma ORM 7 uses the query compiler by default, so you must use a driver adapter.
1. Install adapter and driver:
```bash
npm install @prisma/adapter-pg pg
```
2. Instantiate Prisma Client with the adapter:
```typescript
import 'dotenv/config'
import { PrismaClient } from '../generated/client'
import { PrismaPg } from '@prisma/adapter-pg'
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL })
const prisma = new PrismaClient({ adapter })
```
## Common Issues
### "Can't reach database server"
- Check host and port
- Check firewall settings
- Ensure database is running
### "Authentication failed"
- Check user/password
- Special characters in password must be URL-encoded
### "Schema does not exist"
- Ensure `?schema=public` (or your schema) is in the URL

View File

@@ -0,0 +1,47 @@
# Prisma Client Setup
Generate and instantiate Prisma Client for any database provider.
## 1. Install dependencies
```bash
npm install prisma --save-dev
npm install @prisma/client
```
## 2. Add generator block
In `prisma/schema.prisma`:
```prisma
generator client {
provider = "prisma-client"
output = "../generated"
}
```
Prisma v7 requires an explicit `output` path and will not generate into `node_modules` by default.
## 3. Generate Prisma Client
```bash
npx prisma generate
```
Re-run `prisma generate` after every schema change to keep the client in sync.
## 4. Instantiate Prisma Client
```typescript
import { PrismaClient } from '../generated/client'
import { PrismaPg } from '@prisma/adapter-pg'
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL })
const prisma = new PrismaClient({ adapter })
```
If you change the generator `output`, update the import path to match. In Prisma ORM 7, a **driver adapter is required** — replace `PrismaPg` with the adapter for your database.
## 5. Use a single instance
Each `PrismaClient` instance creates a connection pool. Reuse a single instance per app process to avoid exhausting database connections.

View File

@@ -0,0 +1,90 @@
# Prisma Postgres Setup
Configure Prisma with Prisma Postgres (Managed).
## Overview
Prisma Postgres is a serverless, managed PostgreSQL database optimized for Prisma.
## Setup via CLI
You can provision a Prisma Postgres instance directly via the CLI:
```bash
prisma init --db
```
This will:
1. Log you into Prisma Data Platform.
2. Create a new project and database instance.
3. Update your `.env` with the connection string.
## Connection String
The connection string starts with `prisma+postgres://`.
```env
DATABASE_URL="prisma+postgres://api_key@host.prisma-data.net/env_id"
```
## 1. Schema Configuration
In `prisma/schema.prisma`:
```prisma
datasource db {
provider = "postgresql" // Use postgresql provider
}
generator client {
provider = "prisma-client"
output = "../generated"
}
```
## 2. Config Configuration
In `prisma.config.ts`:
```typescript
import { defineConfig, env } from 'prisma/config'
export default defineConfig({
schema: 'prisma/schema.prisma',
datasource: {
url: env('DATABASE_URL'),
},
})
```
## Driver Adapter (Prisma ORM 7 required)
Prisma ORM 7 uses the query compiler by default, so you must use a driver adapter. For Prisma Postgres, use the Prisma Postgres serverless driver adapter.
1. Install adapter and driver:
```bash
npm install @prisma/adapter-ppg @prisma/ppg
```
2. Use a **direct TCP** connection string for the adapter (from the Prisma Console) and instantiate Prisma Client:
```typescript
import 'dotenv/config'
import { PrismaClient } from '../generated/client'
import { PrismaPostgresAdapter } from '@prisma/adapter-ppg'
const prisma = new PrismaClient({
adapter: new PrismaPostgresAdapter({
connectionString: process.env.PRISMA_DIRECT_TCP_URL,
}),
})
```
## Features
- **Serverless**: Scales to zero.
- **Caching**: Integrated query caching (Accelerate).
- **Real-time**: Database events (Pulse).
## Using with Prisma Client
Since Prisma ORM 7 requires a driver adapter, use the Prisma Postgres adapter shown above when instantiating Prisma Client.

View File

@@ -0,0 +1,106 @@
# SQLite Setup
Configure Prisma with SQLite.
## Prerequisites
- None (file-based)
## 1. Schema Configuration
In `prisma/schema.prisma`:
```prisma
datasource db {
provider = "sqlite"
}
generator client {
provider = "prisma-client"
output = "../generated"
}
```
## 2. Config Configuration (v7)
In `prisma.config.ts`:
```typescript
import { defineConfig, env } from 'prisma/config'
export default defineConfig({
schema: 'prisma/schema.prisma',
datasource: {
url: env('DATABASE_URL'),
},
})
```
## 3. Environment Variable
In `.env`:
```env
DATABASE_URL="file:./dev.db"
```
### Connection String Format
```
file:PATH
```
- **PATH**: Relative path to the database file (from `prisma/schema.prisma` location usually, but in v7 check `prisma.config.ts` context). Usually relative to the schema file.
## Driver Adapter (Prisma ORM 7 required)
Prisma ORM 7 uses the query compiler by default, so you must use a driver adapter.
1. Install adapter and driver:
```bash
npm install @prisma/adapter-better-sqlite3 better-sqlite3
```
2. Instantiate Prisma Client with the adapter:
```typescript
import { PrismaClient } from '../generated/client'
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'
const adapter = new PrismaBetterSqlite3({
url: process.env.DATABASE_URL ?? 'file:./dev.db',
})
const prisma = new PrismaClient({ adapter })
```
## Using Driver Adapter (LibSQL / Turso)
For edge compatibility or Turso:
1. Install:
```bash
npm install @prisma/adapter-libsql @libsql/client
```
2. Instantiate:
```typescript
import { PrismaClient } from '../generated/client'
import { PrismaLibSql } from '@prisma/adapter-libsql'
const adapter = new PrismaLibSql({
url: process.env.TURSO_DATABASE_URL,
authToken: process.env.TURSO_AUTH_TOKEN,
})
const prisma = new PrismaClient({ adapter })
```
## Limitations
- **No Enums**: SQLite doesn't support enums (Prisma polyfills them or treats as String).
- **No Scalar Lists**: `String[]` is not supported directly.
- **Concurrency**: Write operations lock the file.
## Common Issues
### "Database file not found"
Ensure the path in `DATABASE_URL` is correct relative to where Prisma is running or the schema file. `file:./dev.db` creates it next to schema.

View File

@@ -0,0 +1,94 @@
# SQL Server Setup
Configure Prisma with Microsoft SQL Server.
## Prerequisites
- SQL Server 2017, 2019, 2022, or Azure SQL
- TCP/IP enabled
## 1. Schema Configuration
In `prisma/schema.prisma`:
```prisma
datasource db {
provider = "sqlserver"
}
generator client {
provider = "prisma-client"
output = "../generated"
}
```
## 2. Config Configuration (v7)
In `prisma.config.ts`:
```typescript
import { defineConfig, env } from 'prisma/config'
export default defineConfig({
schema: 'prisma/schema.prisma',
datasource: {
url: env('DATABASE_URL'),
},
})
```
## 3. Environment Variable
In `.env`:
```env
DATABASE_URL="sqlserver://localhost:1433;database=mydb;user=sa;password=Password123;encrypt=true;trustServerCertificate=true"
```
### Connection String Format
```
sqlserver://HOST:PORT;database=DB;user=USER;password=PASS;encrypt=true;trustServerCertificate=true
```
- **encrypt**: Required for Azure (true).
- **trustServerCertificate**: True for self-signed certs (local dev).
## Driver Adapter (Prisma ORM 7 required)
Prisma ORM 7 uses the query compiler by default, so you must use a driver adapter.
1. Install adapter and driver:
```bash
npm install @prisma/adapter-mssql mssql
```
2. Instantiate Prisma Client with the adapter:
```typescript
import 'dotenv/config'
import { PrismaClient } from '../generated/client'
import { PrismaMssql } from '@prisma/adapter-mssql'
const adapter = new PrismaMssql({
server: 'localhost',
port: 1433,
database: 'mydb',
user: process.env.SQLSERVER_USER,
password: process.env.SQLSERVER_PASSWORD,
options: {
encrypt: true,
trustServerCertificate: true,
},
})
const prisma = new PrismaClient({ adapter })
```
## Common Issues
### "Login failed for user"
- SQL Server auth vs Windows auth. Prisma typically uses SQL Server authentication (username/password).
- Ensure TCP/IP is enabled in SQL Server Configuration Manager.
### "Table not found" (dbo schema)
Prisma assumes `dbo` schema by default. If using another schema, update the model or connection string? SQL Server provider mostly sticks to default schema.

View File

@@ -0,0 +1,113 @@
---
name: tanstack-router-best-practices
description: TanStack Router best practices for type-safe routing, data loading, search params, and navigation. Activate when building React applications with complex routing needs.
---
# TanStack Router Best Practices
Comprehensive guidelines for implementing TanStack Router patterns in React applications. These rules optimize type safety, data loading, navigation, and code organization.
## When to Apply
- Setting up application routing
- Creating new routes and layouts
- Implementing search parameter handling
- Configuring data loaders
- Setting up code splitting
- Integrating with TanStack Query
- Refactoring navigation patterns
## Rule Categories by Priority
| Priority | Category | Rules | Impact |
|----------|----------|-------|--------|
| CRITICAL | Type Safety | 4 rules | Prevents runtime errors and enables refactoring |
| CRITICAL | Route Organization | 5 rules | Ensures maintainable route structure |
| HIGH | Router Config | 1 rule | Global router defaults |
| HIGH | Data Loading | 6 rules | Optimizes data fetching and caching |
| HIGH | Search Params | 5 rules | Enables type-safe URL state |
| HIGH | Error Handling | 1 rule | Handles 404 and errors gracefully |
| MEDIUM | Navigation | 5 rules | Improves UX and accessibility |
| MEDIUM | Code Splitting | 3 rules | Reduces bundle size |
| MEDIUM | Preloading | 3 rules | Improves perceived performance |
| LOW | Route Context | 3 rules | Enables dependency injection |
## Quick Reference
### Type Safety (Prefix: `ts-`)
- `ts-register-router` — Register router type for global inference
- `ts-use-from-param` — Use `from` parameter for type narrowing
- `ts-route-context-typing` — Type route context with createRootRouteWithContext
- `ts-query-options-loader` — Use queryOptions in loaders for type inference
### Router Config (Prefix: `router-`)
- `router-default-options` — Configure router defaults (scrollRestoration, defaultErrorComponent, etc.)
### Route Organization (Prefix: `org-`)
- `org-file-based-routing` — Prefer file-based routing for conventions
- `org-route-tree-structure` — Follow hierarchical route tree patterns
- `org-pathless-layouts` — Use pathless routes for shared layouts
- `org-index-routes` — Understand index vs layout routes
- `org-virtual-routes` — Understand virtual file routes
### Data Loading (Prefix: `load-`)
- `load-use-loaders` — Use route loaders for data fetching
- `load-loader-deps` — Define loaderDeps for cache control
- `load-ensure-query-data` — Use ensureQueryData with TanStack Query
- `load-deferred-data` — Split critical and non-critical data
- `load-error-handling` — Handle loader errors appropriately
- `load-parallel` — Leverage parallel route loading
### Search Params (Prefix: `search-`)
- `search-validation` — Always validate search params
- `search-type-inheritance` — Leverage parent search param types
- `search-middleware` — Use search param middleware
- `search-defaults` — Provide sensible defaults
- `search-custom-serializer` — Configure custom search param serializers
### Error Handling (Prefix: `err-`)
- `err-not-found` — Handle not-found routes properly
### Navigation (Prefix: `nav-`)
- `nav-link-component` — Prefer Link component for navigation
- `nav-active-states` — Configure active link states
- `nav-use-navigate` — Use useNavigate for programmatic navigation
- `nav-relative-paths` — Understand relative path navigation
- `nav-route-masks` — Use route masks for modal URLs
### Code Splitting (Prefix: `split-`)
- `split-lazy-routes` — Use .lazy.tsx for code splitting
- `split-critical-path` — Keep critical config in main route file
- `split-auto-splitting` — Enable autoCodeSplitting when possible
### Preloading (Prefix: `preload-`)
- `preload-intent` — Enable intent-based preloading
- `preload-stale-time` — Configure preload stale time
- `preload-manual` — Use manual preloading strategically
### Route Context (Prefix: `ctx-`)
- `ctx-root-context` — Define context at root route
- `ctx-before-load` — Extend context in beforeLoad
- `ctx-dependency-injection` — Use context for dependency injection
## How to Use
Each rule file in the `rules/` directory contains:
1. **Explanation** — Why this pattern matters
2. **Bad Example** — Anti-pattern to avoid
3. **Good Example** — Recommended implementation
4. **Context** — When to apply or skip this rule
## Full Reference
See individual rule files in `rules/` directory for detailed guidance and code examples.

View File

@@ -0,0 +1,172 @@
# ctx-root-context: Define Context at Root Route
## Priority: LOW
## Explanation
Use `createRootRouteWithContext` to define typed context that flows through your entire route tree. This enables dependency injection for things like query clients, auth state, and services.
## Bad Example
```tsx
// No context - importing globals directly
// routes/__root.tsx
import { createRootRoute } from '@tanstack/react-router'
import { queryClient } from '@/lib/query-client' // Global import
export const Route = createRootRoute({
component: RootComponent,
})
// routes/posts.tsx
import { queryClient } from '@/lib/query-client' // Import again
export const Route = createFileRoute('/posts')({
loader: async () => {
// Using global - harder to test, couples to implementation
return queryClient.ensureQueryData(postQueries.list())
},
})
```
## Good Example
```tsx
// routes/__root.tsx
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router'
import { QueryClient } from '@tanstack/react-query'
// Define the context interface
interface RouterContext {
queryClient: QueryClient
auth: {
user: User | null
isAuthenticated: boolean
}
}
export const Route = createRootRouteWithContext<RouterContext>()({
component: RootComponent,
})
function RootComponent() {
return (
<>
<Header />
<main>
<Outlet />
</main>
<Footer />
</>
)
}
// router.tsx - Provide context when creating router
import { createRouter } from '@tanstack/react-router'
import { QueryClient } from '@tanstack/react-query'
import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query'
import { routeTree } from './routeTree.gen'
export function getRouter(auth: RouterContext['auth'] = { user: null, isAuthenticated: false }) {
const queryClient = new QueryClient()
const router = createRouter({
routeTree,
context: {
queryClient,
auth,
},
defaultPreload: 'intent',
defaultPreloadStaleTime: 0,
scrollRestoration: true,
})
setupRouterSsrQueryIntegration({ router, queryClient })
return router
}
// routes/posts.tsx - Use context in loaders
export const Route = createFileRoute('/posts')({
loader: async ({ context: { queryClient } }) => {
// Context is typed and injected
return queryClient.ensureQueryData(postQueries.list())
},
})
```
## Good Example: Auth-Protected Routes
```tsx
// routes/__root.tsx
interface RouterContext {
queryClient: QueryClient
auth: AuthState
}
export const Route = createRootRouteWithContext<RouterContext>()({
component: RootComponent,
})
// routes/_authenticated.tsx - Layout route for protected pages
export const Route = createFileRoute('/_authenticated')({
beforeLoad: async ({ context, location }) => {
if (!context.auth.isAuthenticated) {
throw redirect({
to: '/login',
search: { redirect: location.href },
})
}
},
component: AuthenticatedLayout,
})
// routes/_authenticated/dashboard.tsx
export const Route = createFileRoute('/_authenticated/dashboard')({
loader: async ({ context: { queryClient, auth } }) => {
// We know user is authenticated from parent beforeLoad
return queryClient.ensureQueryData(
dashboardQueries.forUser(auth.user!.id)
)
},
})
```
## Extending Context with beforeLoad
```tsx
// routes/posts/$postId.tsx
export const Route = createFileRoute('/posts/$postId')({
beforeLoad: async ({ context, params }) => {
// Extend context with route-specific data
const post = await fetchPost(params.postId)
return {
post, // Available to this route and children
}
},
loader: async ({ context }) => {
// context now includes 'post' from beforeLoad
const comments = await fetchComments(context.post.id)
return { comments }
},
})
```
## Context vs. Loader Data
| Context | Loader Data |
|---------|-------------|
| Available in beforeLoad, loader, and component | Only available in component |
| Set at router creation or in beforeLoad | Returned from loader |
| Good for services, clients, auth | Good for route-specific data |
| Flows down to all children | Specific to route |
## Context
- Type the context interface in `createRootRouteWithContext<T>()`
- Provide context when calling `createRouter({ context: {...} })`
- Context flows from root to all nested routes
- Use `beforeLoad` to extend context for specific subtrees
- Enables testability - inject mocks in tests
- Avoids global imports and singletons

View File

@@ -0,0 +1,194 @@
# err-not-found: Handle Not-Found Routes Properly
## Priority: HIGH
## Explanation
Configure `notFoundComponent` to handle 404 errors gracefully. TanStack Router provides not-found handling at multiple levels: root, route-specific, and programmatic via `notFound()`. Proper configuration prevents blank screens and improves UX.
## Bad Example
```tsx
// No not-found handling - shows blank screen or error
const router = createRouter({
routeTree,
// Missing defaultNotFoundComponent
})
// Or throwing generic error
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
const post = await fetchPost(params.postId)
if (!post) {
throw new Error('Not found') // Generic error, not proper 404
}
return post
},
})
```
## Good Example: Root-Level Not Found
```tsx
// routes/__root.tsx
export const Route = createRootRoute({
component: RootComponent,
notFoundComponent: GlobalNotFound,
})
function GlobalNotFound() {
return (
<div className="not-found">
<h1>404 - Page Not Found</h1>
<p>The page you're looking for doesn't exist.</p>
<Link to="/">Go Home</Link>
</div>
)
}
// router.tsx - Can also set default
const router = createRouter({
routeTree,
defaultNotFoundComponent: () => (
<div>
<h1>404</h1>
<Link to="/">Return Home</Link>
</div>
),
})
```
## Good Example: Route-Specific Not Found
```tsx
// routes/posts/$postId.tsx
import { createFileRoute, notFound } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
const post = await fetchPost(params.postId)
if (!post) {
throw notFound() // Proper 404 handling
}
return post
},
notFoundComponent: PostNotFound, // Custom 404 for this route
component: PostPage,
})
function PostNotFound() {
const { postId } = Route.useParams()
return (
<div>
<h1>Post Not Found</h1>
<p>No post exists with ID: {postId}</p>
<Link to="/posts">Browse all posts</Link>
</div>
)
}
```
## Good Example: Not Found with Data
```tsx
export const Route = createFileRoute('/users/$username')({
loader: async ({ params }) => {
const user = await fetchUser(params.username)
if (!user) {
throw notFound({
// Pass data to notFoundComponent
data: {
username: params.username,
suggestions: await fetchSimilarUsernames(params.username),
},
})
}
return user
},
notFoundComponent: UserNotFound,
})
function UserNotFound() {
const { data } = Route.useMatch()
return (
<div>
<h1>User @{data?.username} not found</h1>
{data?.suggestions?.length > 0 && (
<div>
<p>Did you mean:</p>
<ul>
{data.suggestions.map((username) => (
<li key={username}>
<Link to="/users/$username" params={{ username }}>
@{username}
</Link>
</li>
))}
</ul>
</div>
)}
</div>
)
}
```
## Good Example: Catch-All Route
```tsx
// routes/$.tsx - Catch-all splat route
export const Route = createFileRoute('/$')({
component: CatchAllNotFound,
})
function CatchAllNotFound() {
const { _splat } = Route.useParams()
return (
<div>
<h1>Page Not Found</h1>
<p>No page exists at: /{_splat}</p>
<Link to="/">Go to homepage</Link>
</div>
)
}
```
## Good Example: Nested Not Found Bubbling
```tsx
// Not found bubbles up through route tree
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
notFoundComponent: PostsNotFound, // Catches child 404s too
})
// routes/posts/$postId.tsx
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
const post = await fetchPost(params.postId)
if (!post) throw notFound()
return post
},
// No notFoundComponent - bubbles to parent
})
// routes/posts/$postId/comments.tsx
export const Route = createFileRoute('/posts/$postId/comments')({
loader: async ({ params }) => {
const comments = await fetchComments(params.postId)
if (!comments) throw notFound() // Bubbles to /posts notFoundComponent
return comments
},
})
```
## Context
- `notFound()` throws a special error caught by nearest `notFoundComponent`
- Not found bubbles up the route tree if not handled locally
- Use `defaultNotFoundComponent` on router for global fallback
- Pass data to `notFound({ data })` for contextual 404 pages
- Catch-all routes (`/$`) can handle truly unknown paths
- Different from error boundaries - specifically for 404 cases

View File

@@ -0,0 +1,153 @@
# load-ensure-query-data: Use ensureQueryData with TanStack Query
## Priority: HIGH
## Explanation
When integrating TanStack Router with TanStack Query, use `queryClient.ensureQueryData()` in loaders instead of `prefetchQuery()`. This respects the cache, awaits data if missing, and returns the data for potential use.
## Bad Example
```tsx
// Using prefetchQuery - doesn't return data, can't await stale check
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params, context: { queryClient } }) => {
// prefetchQuery never throws, swallows errors
queryClient.prefetchQuery({
queryKey: ['posts', params.postId],
queryFn: () => fetchPost(params.postId),
})
// No await - might not complete before render
// No return value to use
},
})
// Fetching directly - bypasses TanStack Query cache
export const Route = createFileRoute('/posts')({
loader: async () => {
const posts = await fetchPosts() // Not cached
return { posts }
},
})
```
## Good Example
```tsx
// Define queryOptions for reuse
const postQueryOptions = (postId: string) =>
queryOptions({
queryKey: ['posts', postId],
queryFn: () => fetchPost(postId),
staleTime: 5 * 60 * 1000, // 5 minutes
})
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params, context: { queryClient } }) => {
// ensureQueryData:
// - Returns cached data if fresh
// - Fetches and caches if missing or stale
// - Awaits completion
// - Throws on error (caught by error boundary)
await queryClient.ensureQueryData(postQueryOptions(params.postId))
},
component: PostPage,
})
function PostPage() {
const { postId } = Route.useParams()
// Data guaranteed to exist from loader
const { data: post } = useSuspenseQuery(postQueryOptions(postId))
return <PostContent post={post} />
}
```
## Good Example: Multiple Parallel Queries
```tsx
export const Route = createFileRoute('/dashboard')({
loader: async ({ context: { queryClient } }) => {
// Parallel data fetching
await Promise.all([
queryClient.ensureQueryData(statsQueries.overview()),
queryClient.ensureQueryData(activityQueries.recent()),
queryClient.ensureQueryData(notificationQueries.unread()),
])
},
})
```
## Good Example: Dependent Queries
```tsx
export const Route = createFileRoute('/users/$userId/posts')({
loader: async ({ params, context: { queryClient } }) => {
// First query needed for second
const user = await queryClient.ensureQueryData(
userQueries.detail(params.userId)
)
// Dependent query uses result
await queryClient.ensureQueryData(
postQueries.byAuthor(user.id)
)
},
})
```
## Router Configuration for TanStack Query
```tsx
// router.tsx
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute default
},
},
})
export const router = createRouter({
routeTree,
context: { queryClient },
// Let TanStack Query manage caching
defaultPreloadStaleTime: 0,
// SSR: Dehydrate query cache
dehydrate: () => ({
queryClientState: dehydrate(queryClient),
}),
// SSR: Hydrate on client
hydrate: (dehydrated) => {
hydrate(queryClient, dehydrated.queryClientState)
},
// Wrap with QueryClientProvider
Wrap: ({ children }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
),
})
```
## ensureQueryData vs prefetchQuery vs fetchQuery
| Method | Returns | Throws | Awaits | Use Case |
|--------|---------|--------|--------|----------|
| `ensureQueryData` | Data | Yes | Yes | Route loaders (recommended) |
| `prefetchQuery` | void | No | Yes | Background prefetching |
| `fetchQuery` | Data | Yes | Yes | When you need data immediately |
## Context
- `ensureQueryData` is the recommended method for route loaders
- Respects `staleTime` - won't refetch fresh cached data
- Errors propagate to route error boundaries
- Use `queryOptions()` factory for type-safe, reusable query definitions
- Set `defaultPreloadStaleTime: 0` to let TanStack Query manage cache
- Pair with `useSuspenseQuery` in components for guaranteed data

View File

@@ -0,0 +1,190 @@
# load-parallel: Leverage Parallel Route Loading
## Priority: MEDIUM
## Explanation
TanStack Router loads nested route data in parallel, not sequentially. Structure your routes and loaders to maximize parallelization and avoid creating artificial waterfalls.
## Bad Example
```tsx
// Creating waterfall with dependent beforeLoad
export const Route = createFileRoute('/dashboard')({
beforeLoad: async () => {
const user = await fetchUser() // 200ms
const permissions = await fetchPermissions(user.id) // 200ms
const preferences = await fetchPreferences(user.id) // 200ms
// Total: 600ms (sequential)
return { user, permissions, preferences }
},
})
// Or nesting data dependencies incorrectly
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: async () => {
const posts = await fetchPosts() // 300ms
return { posts }
},
})
// routes/posts/$postId.tsx
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
// Waits for parent to complete first - waterfall!
const post = await fetchPost(params.postId) // +200ms
return { post }
},
})
```
## Good Example: Parallel in Single Loader
```tsx
export const Route = createFileRoute('/dashboard')({
beforeLoad: async () => {
// All requests start simultaneously
const [user, config] = await Promise.all([
fetchUser(), // 200ms
fetchAppConfig(), // 150ms
])
// Total: 200ms (parallel)
return { user, config }
},
loader: async ({ context }) => {
// These also run in parallel with each other
const [stats, activity, notifications] = await Promise.all([
fetchDashboardStats(context.user.id),
fetchRecentActivity(context.user.id),
fetchNotifications(context.user.id),
])
return { stats, activity, notifications }
},
})
```
## Good Example: Parallel Nested Routes
```tsx
// Parent and child loaders run in PARALLEL
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: async () => {
// This runs...
const categories = await fetchCategories()
return { categories }
},
})
// routes/posts/$postId.tsx
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
// ...at the SAME TIME as this!
const post = await fetchPost(params.postId)
const comments = await fetchComments(params.postId)
return { post, comments }
},
})
// Navigation to /posts/123:
// - Both loaders start simultaneously
// - Total time = max(categoriesTime, postTime + commentsTime)
// - NOT categoriesTime + postTime + commentsTime
```
## Good Example: With TanStack Query
```tsx
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: async ({ context: { queryClient } }) => {
// These all start in parallel
await Promise.all([
queryClient.ensureQueryData(postQueries.list()),
queryClient.ensureQueryData(categoryQueries.all()),
])
},
})
// routes/posts/$postId.tsx
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params, context: { queryClient } }) => {
// Runs in parallel with parent loader
await Promise.all([
queryClient.ensureQueryData(postQueries.detail(params.postId)),
queryClient.ensureQueryData(commentQueries.forPost(params.postId)),
])
},
})
```
## Good Example: Streaming Non-Critical Data
```tsx
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params, context: { queryClient } }) => {
// Critical data - await
const post = await queryClient.ensureQueryData(
postQueries.detail(params.postId)
)
// Non-critical - start but don't await (stream in later)
queryClient.prefetchQuery(commentQueries.forPost(params.postId))
queryClient.prefetchQuery(relatedQueries.forPost(params.postId))
return { post }
},
component: PostPage,
})
function PostPage() {
const { post } = Route.useLoaderData()
const { postId } = Route.useParams()
// Critical data ready immediately
// Non-critical loads in component with loading state
const { data: comments, isLoading } = useQuery(
commentQueries.forPost(postId)
)
return (
<article>
<PostContent post={post} />
{isLoading ? <CommentsSkeleton /> : <Comments data={comments} />}
</article>
)
}
```
## Route Loading Timeline
```
Navigation to /posts/123
Without parallelization:
├─ beforeLoad (parent) ████████
├─ loader (parent) ████████
├─ beforeLoad (child) ████
├─ loader (child) ████████
└─ Render █
With parallelization:
├─ beforeLoad (parent) ████████
├─ beforeLoad (child) ████
├─ loader (parent) ████████
├─ loader (child) ████████████
└─ Render █
```
## Context
- Nested route loaders run in parallel by default
- `beforeLoad` runs before `loader` (for auth, context setup)
- Use `Promise.all` for parallel fetches within a single loader
- Parent context is available in child loaders (after beforeLoad)
- Prefetch non-critical data without awaiting for streaming
- Monitor network tab to verify parallelization

View File

@@ -0,0 +1,148 @@
# load-use-loaders: Use Route Loaders for Data Fetching
## Priority: HIGH
## Explanation
Route loaders execute before the route renders, enabling data to be ready when the component mounts. This prevents loading waterfalls, enables preloading, and integrates with the router's caching layer.
## Bad Example
```tsx
// Fetching in component - creates waterfall
function PostsPage() {
const [posts, setPosts] = useState<Post[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
// Route renders, THEN data fetches, THEN UI updates
fetchPosts().then((data) => {
setPosts(data)
setLoading(false)
})
}, [])
if (loading) return <Loading />
return <PostList posts={posts} />
}
// No preloading possible - user sees loading state on navigation
```
## Good Example
```tsx
// routes/posts.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts')({
loader: async () => {
const posts = await fetchPosts()
return { posts }
},
component: PostsPage,
})
function PostsPage() {
// Data is ready when component mounts - no loading state needed
const { posts } = Route.useLoaderData()
return <PostList posts={posts} />
}
```
## Good Example: With Parameters
```tsx
// routes/posts/$postId.tsx
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
// params are type-safe and guaranteed to exist
const post = await fetchPost(params.postId)
const comments = await fetchComments(params.postId)
return { post, comments }
},
component: PostDetailPage,
})
function PostDetailPage() {
const { post, comments } = Route.useLoaderData()
const { postId } = Route.useParams()
return (
<article>
<h1>{post.title}</h1>
<PostContent content={post.content} />
<CommentList comments={comments} />
</article>
)
}
```
## Good Example: With TanStack Query
```tsx
// routes/posts/$postId.tsx
import { queryOptions } from '@tanstack/react-query'
const postQueryOptions = (postId: string) =>
queryOptions({
queryKey: ['posts', postId],
queryFn: () => fetchPost(postId),
})
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params, context: { queryClient } }) => {
// Ensure data is in cache before render
await queryClient.ensureQueryData(postQueryOptions(params.postId))
},
component: PostDetailPage,
})
function PostDetailPage() {
const { postId } = Route.useParams()
// useSuspenseQuery because loader guarantees data exists
const { data: post } = useSuspenseQuery(postQueryOptions(postId))
return <PostContent post={post} />
}
```
## Loader Context Properties
```tsx
export const Route = createFileRoute('/posts')({
loader: async ({
params, // Route path parameters
context, // Route context (queryClient, auth, etc.)
abortController, // For cancelling stale requests
cause, // 'enter' | 'preload' | 'stay'
deps, // Dependencies from loaderDeps
preload, // Boolean: true if preloading
}) => {
// Use abortController for fetch cancellation
const response = await fetch('/api/posts', {
signal: abortController.signal,
})
// Different behavior for preload vs navigation
if (preload) {
// Lighter data for preload
return { posts: await response.json() }
}
// Full data for actual navigation
const posts = await response.json()
const stats = await fetchStats()
return { posts, stats }
},
})
```
## Context
- Loaders run during route matching, before component render
- Supports parallel loading across nested routes
- Enables preloading on link hover/focus
- Built-in stale-while-revalidate caching
- For complex caching needs, integrate with TanStack Query
- Use `beforeLoad` for auth checks and redirects

View File

@@ -0,0 +1,178 @@
# nav-link-component: Prefer Link Component for Navigation
## Priority: MEDIUM
## Explanation
Use the `<Link>` component for navigation instead of `useNavigate()` when possible. Links render proper `<a>` tags with valid `href` attributes, enabling right-click → open in new tab, better SEO, and accessibility.
## Bad Example
```tsx
// Using onClick with navigate - loses standard link behavior
function PostCard({ post }: { post: Post }) {
const navigate = useNavigate()
return (
<div
onClick={() => navigate({ to: '/posts/$postId', params: { postId: post.id } })}
className="post-card"
>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</div>
)
}
// Problems:
// - No right-click → open in new tab
// - No cmd/ctrl+click for new tab
// - Not announced as link to screen readers
// - No valid href for SEO
```
## Good Example
```tsx
import { Link } from '@tanstack/react-router'
function PostCard({ post }: { post: Post }) {
return (
<Link
to="/posts/$postId"
params={{ postId: post.id }}
className="post-card"
>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</Link>
)
}
// Benefits:
// - Renders <a href="/posts/123">
// - Right-click menu works
// - Cmd/Ctrl+click opens new tab
// - Screen readers announce as link
// - Preloading works on hover
```
## Good Example: With Search Params
```tsx
function FilteredLink() {
return (
<Link
to="/products"
search={{ category: 'electronics', sort: 'price' }}
>
View Electronics
</Link>
)
}
// Preserving existing search params
function SortLink({ sort }: { sort: 'asc' | 'desc' }) {
return (
<Link
to="." // Current route
search={(prev) => ({ ...prev, sort })}
>
Sort {sort === 'asc' ? 'Ascending' : 'Descending'}
</Link>
)
}
```
## Good Example: With Active States
```tsx
function NavLink({ to, children }: { to: string; children: React.ReactNode }) {
return (
<Link
to={to}
activeProps={{
className: 'nav-link-active',
'aria-current': 'page',
}}
inactiveProps={{
className: 'nav-link',
}}
activeOptions={{
exact: true, // Only active on exact match
}}
>
{children}
</Link>
)
}
// Or use render props for more control
function CustomNavLink({ to, children }: { to: string; children: React.ReactNode }) {
return (
<Link to={to}>
{({ isActive }) => (
<span className={isActive ? 'text-blue-600 font-bold' : 'text-gray-600'}>
{children}
{isActive && <CheckIcon className="ml-2" />}
</span>
)}
</Link>
)
}
```
## Good Example: With Preloading
```tsx
function PostList({ posts }: { posts: Post[] }) {
return (
<ul>
{posts.map(post => (
<li key={post.id}>
<Link
to="/posts/$postId"
params={{ postId: post.id }}
preload="intent" // Preload on hover/focus
preloadDelay={100} // Wait 100ms before preloading
>
{post.title}
</Link>
</li>
))}
</ul>
)
}
```
## When to Use useNavigate Instead
```tsx
// 1. After form submission
const createPost = useMutation({
mutationFn: submitPost,
onSuccess: (data) => {
navigate({ to: '/posts/$postId', params: { postId: data.id } })
},
})
// 2. After authentication
async function handleLogin(credentials: Credentials) {
await login(credentials)
navigate({ to: '/dashboard' })
}
// 3. Programmatic redirects
useEffect(() => {
if (!isAuthenticated) {
navigate({ to: '/login', search: { redirect: location.pathname } })
}
}, [isAuthenticated])
```
## Context
- `<Link>` renders actual `<a>` tags with proper `href`
- Supports all standard link behaviors (middle-click, cmd+click, etc.)
- Enables preloading on hover/focus
- Better for SEO - crawlers can follow links
- Reserve `useNavigate` for side effects and programmatic navigation
- Use `<Navigate>` component for immediate redirects on render

View File

@@ -0,0 +1,197 @@
# nav-route-masks: Use Route Masks for Modal URLs
## Priority: LOW
## Explanation
Route masks let you display one URL while internally routing to another. This is useful for modals, sheets, and overlays where you want a shareable URL that shows the modal, but navigating there directly should show the full page.
## Bad Example
```tsx
// Modal without proper URL handling
function PostList() {
const [selectedPost, setSelectedPost] = useState<string | null>(null)
return (
<div>
{posts.map(post => (
<div key={post.id} onClick={() => setSelectedPost(post.id)}>
{post.title}
</div>
))}
{selectedPost && (
<Modal onClose={() => setSelectedPost(null)}>
<PostDetail postId={selectedPost} />
</Modal>
)}
</div>
)
}
// Problems:
// - URL doesn't change when modal opens
// - Can't share link to modal
// - Back button doesn't close modal
// - Refresh loses modal state
```
## Good Example: Route Masks for Modal
```tsx
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
component: PostList,
})
function PostList() {
const posts = usePosts()
return (
<div>
{posts.map(post => (
<Link
key={post.id}
to="/posts/$postId"
params={{ postId: post.id }}
mask={{
to: '/posts',
// URL shows /posts but routes to /posts/$postId
}}
>
{post.title}
</Link>
))}
<Outlet /> {/* Modal renders here */}
</div>
)
}
// routes/posts/$postId.tsx
export const Route = createFileRoute('/posts/$postId')({
component: PostModal,
})
function PostModal() {
const { postId } = Route.useParams()
const navigate = useNavigate()
return (
<Modal onClose={() => navigate({ to: '/posts' })}>
<PostDetail postId={postId} />
</Modal>
)
}
// User clicks post:
// - URL stays /posts (masked)
// - PostModal renders
// - Share link goes to /posts/$postId (real URL)
// - Direct navigation to /posts/$postId shows full page (no mask)
```
## Good Example: With Search Params
```tsx
function PostList() {
return (
<div>
{posts.map(post => (
<Link
key={post.id}
to="/posts/$postId"
params={{ postId: post.id }}
mask={{
to: '/posts',
search: { modal: post.id }, // /posts?modal=123
}}
>
{post.title}
</Link>
))}
</div>
)
}
```
## Good Example: Programmatic Navigation with Mask
```tsx
function PostCard({ post }: { post: Post }) {
const navigate = useNavigate()
const openInModal = () => {
navigate({
to: '/posts/$postId',
params: { postId: post.id },
mask: {
to: '/posts',
},
})
}
const openFullPage = () => {
navigate({
to: '/posts/$postId',
params: { postId: post.id },
// No mask - shows real URL
})
}
return (
<div>
<h3>{post.title}</h3>
<button onClick={openInModal}>Quick View</button>
<button onClick={openFullPage}>Full Page</button>
</div>
)
}
```
## Good Example: Unmask on Interaction
```tsx
function PostModal() {
const { postId } = Route.useParams()
const navigate = useNavigate()
const expandToFullPage = () => {
// Navigate to real URL, removing mask
navigate({
to: '/posts/$postId',
params: { postId },
// No mask = real URL
replace: true, // Replace history entry
})
}
return (
<Modal>
<PostDetail postId={postId} />
<button onClick={expandToFullPage}>
Expand to full page
</button>
</Modal>
)
}
```
## Route Mask Behavior
| Scenario | URL Shown | Actual Route |
|----------|-----------|--------------|
| Click masked link | Masked URL | Real route |
| Share/copy URL | Real URL | Real route |
| Direct navigation | Real URL | Real route |
| Browser refresh | Depends on URL in bar | Matches URL |
| Back button | Previous URL | Previous route |
## Context
- Masks are client-side only - shared URLs are the real route
- Direct navigation to real URL bypasses mask (shows full page)
- Back button navigates through history correctly
- Use for modals, side panels, quick views
- Masks can include different search params
- Consider UX: users expect shared URLs to work

View File

@@ -0,0 +1,133 @@
# org-virtual-routes: Understand Virtual File Routes
## Priority: LOW
## Explanation
Virtual routes are automatically generated placeholder routes in the route tree when you have a `.lazy.tsx` file without a corresponding main route file. They provide the minimal configuration needed to anchor lazy-loaded components.
## Bad Example
```tsx
// Creating unnecessary boilerplate main route files
// routes/settings.tsx - Just to have a file
export const Route = createFileRoute('/settings')({
// Empty - no loader, no beforeLoad, nothing
})
// routes/settings.lazy.tsx - Actual component
export const Route = createLazyFileRoute('/settings')({
component: SettingsPage,
})
// The main file is unnecessary boilerplate
```
## Good Example: Let Virtual Routes Handle It
```tsx
// Delete routes/settings.tsx entirely!
// routes/settings.lazy.tsx - Only file needed
export const Route = createLazyFileRoute('/settings')({
component: SettingsPage,
pendingComponent: SettingsLoading,
errorComponent: SettingsError,
})
function SettingsPage() {
return <div>Settings Content</div>
}
// TanStack Router auto-generates a virtual route:
// {
// path: '/settings',
// // Minimal config to anchor the lazy file
// }
```
## Good Example: When You DO Need Main Route File
```tsx
// routes/dashboard.tsx - Need this for loader/beforeLoad
export const Route = createFileRoute('/dashboard')({
beforeLoad: async ({ context }) => {
if (!context.auth.isAuthenticated) {
throw redirect({ to: '/login' })
}
},
loader: async ({ context: { queryClient } }) => {
await queryClient.ensureQueryData(dashboardQueries.stats())
},
// Component is in lazy file
})
// routes/dashboard.lazy.tsx
export const Route = createLazyFileRoute('/dashboard')({
component: DashboardPage,
pendingComponent: DashboardSkeleton,
})
// Main file IS needed here because we have loader/beforeLoad
```
## Decision Guide
| Route Has... | Need Main File? | Use Virtual? |
|--------------|-----------------|--------------|
| Only component | No | Yes |
| loader | Yes | No |
| beforeLoad | Yes | No |
| validateSearch | Yes | No |
| loaderDeps | Yes | No |
| Just pendingComponent/errorComponent | No | Yes |
## Good Example: File Structure with Virtual Routes
```
routes/
├── __root.tsx # Always needed
├── index.tsx # Has loader
├── about.lazy.tsx # Virtual route (no main file)
├── contact.lazy.tsx # Virtual route (no main file)
├── dashboard.tsx # Has beforeLoad (auth)
├── dashboard.lazy.tsx # Component
├── posts.tsx # Has loader
├── posts.lazy.tsx # Component
├── posts/
│ ├── $postId.tsx # Has loader
│ └── $postId.lazy.tsx # Component
└── settings/
├── index.lazy.tsx # Virtual route
├── profile.lazy.tsx # Virtual route
└── security.tsx # Has beforeLoad (requires re-auth)
```
## Good Example: Generated Route Tree
```tsx
// routeTree.gen.ts (auto-generated)
import { Route as rootRoute } from './routes/__root'
import { Route as aboutLazyRoute } from './routes/about.lazy' // Virtual parent
export const routeTree = rootRoute.addChildren([
// Virtual route created for about.lazy.tsx
createRoute({
path: '/about',
getParentRoute: () => rootRoute,
}).lazy(() => import('./routes/about.lazy').then(m => m.Route)),
// Regular route with explicit main file
dashboardRoute.addChildren([...]),
])
```
## Context
- Virtual routes reduce boilerplate for simple pages
- Only works with file-based routing
- Auto-generated in `routeTree.gen.ts`
- Main route file needed for any "critical path" config
- Critical: loader, beforeLoad, validateSearch, loaderDeps, context
- Non-critical (can be in lazy): component, pendingComponent, errorComponent
- Check generated route tree to verify virtual routes

View File

@@ -0,0 +1,133 @@
# preload-intent: Enable Intent-Based Preloading
## Priority: MEDIUM
## Explanation
Configure `defaultPreload: 'intent'` to preload routes when users hover or focus links. This loads data before the click, making navigation feel instant.
## Bad Example
```tsx
// No preloading configured - data loads after click
const router = createRouter({
routeTree,
// No defaultPreload - user waits after every navigation
})
// Each navigation shows loading state
function PostList({ posts }: { posts: Post[] }) {
return (
<ul>
{posts.map(post => (
<li key={post.id}>
<Link to="/posts/$postId" params={{ postId: post.id }}>
{post.title}
</Link>
{/* Click → wait for data → render */}
</li>
))}
</ul>
)
}
```
## Good Example
```tsx
// router.tsx - Enable preloading by default
const router = createRouter({
routeTree,
defaultPreload: 'intent', // Preload on hover/focus
defaultPreloadDelay: 50, // Wait 50ms before starting
})
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
// Links automatically preload on hover
function PostList({ posts }: { posts: Post[] }) {
return (
<ul>
{posts.map(post => (
<li key={post.id}>
<Link to="/posts/$postId" params={{ postId: post.id }}>
{post.title}
</Link>
{/* Hover → preload starts → click → instant navigation */}
</li>
))}
</ul>
)
}
```
## Preload Options
```tsx
// Router-level defaults
const router = createRouter({
routeTree,
defaultPreload: 'intent', // 'intent' | 'render' | 'viewport' | false
defaultPreloadDelay: 50, // ms before preload starts
defaultPreloadStaleTime: 30000, // 30s - how long preloaded data stays fresh
})
// Link-level overrides
<Link
to="/heavy-page"
preload={false} // Disable for this specific link
>
Heavy Page
</Link>
<Link
to="/critical-page"
preload="render" // Preload immediately when Link renders
>
Critical Page
</Link>
```
## Preload Strategies
| Strategy | Behavior | Use Case |
|----------|----------|----------|
| `'intent'` | Preload on hover/focus | Default for most links |
| `'render'` | Preload when Link mounts | Critical next pages |
| `'viewport'` | Preload when Link enters viewport | Below-fold content |
| `false` | No preloading | Heavy, rarely-visited pages |
## Good Example: With TanStack Query Integration
```tsx
// When using TanStack Query, disable router cache
const router = createRouter({
routeTree,
defaultPreload: 'intent',
defaultPreloadStaleTime: 0, // Let TanStack Query manage cache
context: {
queryClient,
},
})
// Route loader uses TanStack Query
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params, context: { queryClient } }) => {
// ensureQueryData respects TanStack Query's staleTime
await queryClient.ensureQueryData(postQueries.detail(params.postId))
},
})
```
## Context
- Preloading loads route code AND executes loaders
- `preloadDelay` prevents excessive requests on quick mouse movements
- Preloaded data is garbage collected after `preloadStaleTime`
- Works with both router caching and external caching (TanStack Query)
- Mobile: Consider `'viewport'` since hover isn't available
- Monitor network tab to verify preloading works correctly

View File

@@ -0,0 +1,163 @@
# router-default-options: Configure Router Default Options
## Priority: HIGH
## Explanation
TanStack Router's `createRouter` accepts several default options that apply globally. Configure these for consistent behavior across your application including error handling, scroll restoration, and performance optimizations.
## Bad Example
```tsx
// Minimal router - missing useful defaults
const router = createRouter({
routeTree,
context: { queryClient },
})
// Each route must handle its own errors
// No scroll restoration on navigation
// No preloading configured
```
## Good Example: Full Configuration
```tsx
import { QueryClient } from '@tanstack/react-query'
import { createRouter } from '@tanstack/react-router'
import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query'
import { routeTree } from './routeTree.gen'
import { DefaultCatchBoundary } from '@/components/DefaultCatchBoundary'
import { DefaultNotFound } from '@/components/DefaultNotFound'
export function getRouter() {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
staleTime: 1000 * 60 * 2,
},
},
})
const router = createRouter({
routeTree,
context: { queryClient, user: null },
// Preloading
defaultPreload: 'intent', // Preload on hover/focus
defaultPreloadStaleTime: 0, // Let Query manage freshness
// Error handling
defaultErrorComponent: DefaultCatchBoundary,
defaultNotFoundComponent: DefaultNotFound,
// UX
scrollRestoration: true, // Restore scroll on back/forward
// Performance
defaultStructuralSharing: true, // Optimize re-renders
})
setupRouterSsrQueryIntegration({
router,
queryClient,
})
return router
}
```
## Good Example: DefaultCatchBoundary Component
```tsx
// components/DefaultCatchBoundary.tsx
import { ErrorComponent, useRouter } from '@tanstack/react-router'
export function DefaultCatchBoundary({ error }: { error: Error }) {
const router = useRouter()
return (
<div className="error-container">
<h1>Something went wrong</h1>
<ErrorComponent error={error} />
<button onClick={() => router.invalidate()}>
Try again
</button>
</div>
)
}
```
## Good Example: DefaultNotFound Component
```tsx
// components/DefaultNotFound.tsx
import { Link } from '@tanstack/react-router'
export function DefaultNotFound() {
return (
<div className="not-found-container">
<h1>404 - Page Not Found</h1>
<p>The page you're looking for doesn't exist.</p>
<Link to="/">Go home</Link>
</div>
)
}
```
## Router Options Reference
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `defaultPreload` | `false \| 'intent' \| 'render' \| 'viewport'` | `false` | When to preload routes |
| `defaultPreloadStaleTime` | `number` | `30000` | How long preloaded data stays fresh (ms) |
| `defaultErrorComponent` | `Component` | Built-in | Global error boundary |
| `defaultNotFoundComponent` | `Component` | Built-in | Global 404 page |
| `scrollRestoration` | `boolean` | `false` | Restore scroll on navigation |
| `defaultStructuralSharing` | `boolean` | `true` | Optimize loader data re-renders |
## Good Example: Route-Level Overrides
```tsx
// Routes can override defaults
export const Route = createFileRoute('/admin')({
// Custom error handling for admin section
errorComponent: AdminErrorBoundary,
notFoundComponent: AdminNotFound,
// Disable preload for sensitive routes
preload: false,
})
```
## Good Example: With Pending Component
```tsx
const router = createRouter({
routeTree,
context: { queryClient },
defaultPreload: 'intent',
defaultPreloadStaleTime: 0,
defaultErrorComponent: DefaultCatchBoundary,
defaultNotFoundComponent: DefaultNotFound,
scrollRestoration: true,
// Show during route transitions
defaultPendingComponent: () => (
<div className="loading-bar" />
),
defaultPendingMinMs: 200, // Min time to show pending UI
defaultPendingMs: 1000, // Delay before showing pending UI
})
```
## Context
- Set `defaultPreloadStaleTime: 0` when using TanStack Query
- `scrollRestoration: true` improves back/forward navigation UX
- `defaultStructuralSharing` prevents unnecessary re-renders
- Route-level options override router defaults
- Error/NotFound components receive route context
- Pending components help with perceived performance

View File

@@ -0,0 +1,198 @@
# search-custom-serializer: Configure Custom Search Param Serializers
## Priority: LOW
## Explanation
By default, TanStack Router serializes search params as JSON. For cleaner URLs or compatibility with external systems, you can provide custom serializers using libraries like `qs`, `query-string`, or your own implementation.
## Bad Example
```tsx
// Default JSON serialization creates ugly URLs
// URL: /products?filters=%7B%22category%22%3A%22electronics%22%2C%22inStock%22%3Atrue%7D
// Or manually parsing/serializing inconsistently
function ProductList() {
const searchParams = new URLSearchParams(window.location.search)
const filters = JSON.parse(searchParams.get('filters') || '{}')
// Inconsistent with router's handling
}
```
## Good Example: Using JSURL for Compact URLs
```tsx
import { createRouter } from '@tanstack/react-router'
import JSURL from 'jsurl2'
const router = createRouter({
routeTree,
search: {
// Custom serializer for compact, URL-safe encoding
serialize: (search) => JSURL.stringify(search),
parse: (searchString) => JSURL.parse(searchString) || {},
},
})
// URL: /products?~(category~'electronics~inStock~true)
// Much shorter than JSON!
```
## Good Example: Using query-string for Flat Params
```tsx
import { createRouter } from '@tanstack/react-router'
import queryString from 'query-string'
const router = createRouter({
routeTree,
search: {
serialize: (search) =>
queryString.stringify(search, {
arrayFormat: 'bracket',
skipNull: true,
}),
parse: (searchString) =>
queryString.parse(searchString, {
arrayFormat: 'bracket',
parseBooleans: true,
parseNumbers: true,
}),
},
})
// URL: /products?category=electronics&inStock=true&tags[]=sale&tags[]=new
// Traditional query string format
```
## Good Example: Using qs for Nested Objects
```tsx
import { createRouter } from '@tanstack/react-router'
import qs from 'qs'
const router = createRouter({
routeTree,
search: {
serialize: (search) =>
qs.stringify(search, {
encodeValuesOnly: true,
arrayFormat: 'brackets',
}),
parse: (searchString) =>
qs.parse(searchString, {
ignoreQueryPrefix: true,
decoder(value) {
// Parse booleans and numbers
if (value === 'true') return true
if (value === 'false') return false
if (/^-?\d+$/.test(value)) return parseInt(value, 10)
return value
},
}),
},
})
// URL: /products?filters[category]=electronics&filters[price][min]=100&filters[price][max]=500
```
## Good Example: Base64 for Complex State
```tsx
import { createRouter } from '@tanstack/react-router'
const router = createRouter({
routeTree,
search: {
serialize: (search) => {
if (Object.keys(search).length === 0) return ''
const json = JSON.stringify(search)
return btoa(json) // Base64 encode
},
parse: (searchString) => {
if (!searchString) return {}
try {
return JSON.parse(atob(searchString)) // Base64 decode
} catch {
return {}
}
},
},
})
// URL: /products?eyJjYXRlZ29yeSI6ImVsZWN0cm9uaWNzIn0
// Opaque but compact
```
## Good Example: Hybrid Approach
```tsx
// Some params as regular query, complex ones as JSON
import { createRouter } from '@tanstack/react-router'
const router = createRouter({
routeTree,
search: {
serialize: (search) => {
const { filters, ...simple } = search
const params = new URLSearchParams()
// Simple values as regular params
Object.entries(simple).forEach(([key, value]) => {
if (value !== undefined) {
params.set(key, String(value))
}
})
// Complex filters as JSON
if (filters && Object.keys(filters).length > 0) {
params.set('filters', JSON.stringify(filters))
}
return params.toString()
},
parse: (searchString) => {
const params = new URLSearchParams(searchString)
const result: Record<string, unknown> = {}
params.forEach((value, key) => {
if (key === 'filters') {
result.filters = JSON.parse(value)
} else if (value === 'true') {
result[key] = true
} else if (value === 'false') {
result[key] = false
} else if (/^-?\d+$/.test(value)) {
result[key] = parseInt(value, 10)
} else {
result[key] = value
}
})
return result
},
},
})
// URL: /products?page=1&sort=price&filters={"category":"electronics","inStock":true}
```
## Serializer Comparison
| Library | URL Style | Best For |
|---------|-----------|----------|
| Default (JSON) | `?data=%7B...%7D` | TypeScript safety |
| jsurl2 | `?~(key~'value)` | Compact, readable |
| query-string | `?key=value&arr[]=1` | Traditional APIs |
| qs | `?obj[nested]=value` | Deep nesting |
| Base64 | `?eyJrZXkiOiJ2YWx1ZSJ9` | Opaque, compact |
## Context
- Custom serializers apply globally to all routes
- Route-level `validateSearch` still works after parsing
- Consider URL length limits (~2000 chars for safe cross-browser)
- SEO: Search engines may not understand custom formats
- Bookmarkability: Users can't easily modify opaque URLs
- Debugging: JSON is easier to read in browser devtools

View File

@@ -0,0 +1,158 @@
# search-validation: Always Validate Search Params
## Priority: HIGH
## Explanation
Search params come from the URL - user-controlled input that must be validated. Use `validateSearch` to parse, validate, and provide defaults. This ensures type safety and prevents runtime errors from malformed URLs.
## Bad Example
```tsx
// No validation - trusting URL input directly
export const Route = createFileRoute('/products')({
component: ProductsPage,
})
function ProductsPage() {
// Accessing raw search params without validation
const searchParams = new URLSearchParams(window.location.search)
const page = parseInt(searchParams.get('page') || '1') // Could be NaN
const sort = searchParams.get('sort') as 'asc' | 'desc' // Could be anything
// Runtime errors possible if URL is malformed
return <ProductList page={page} sort={sort} />
}
```
## Good Example: Manual Validation
```tsx
export const Route = createFileRoute('/products')({
validateSearch: (search: Record<string, unknown>) => {
return {
page: Number(search.page) || 1,
sort: search.sort === 'desc' ? 'desc' : 'asc',
category: typeof search.category === 'string' ? search.category : undefined,
minPrice: Number(search.minPrice) || undefined,
maxPrice: Number(search.maxPrice) || undefined,
}
},
component: ProductsPage,
})
function ProductsPage() {
// Fully typed, validated search params
const { page, sort, category, minPrice, maxPrice } = Route.useSearch()
// page: number (default 1)
// sort: 'asc' | 'desc' (default 'asc')
// category: string | undefined
}
```
## Good Example: With Zod
```tsx
import { z } from 'zod'
const productSearchSchema = z.object({
page: z.number().min(1).catch(1),
limit: z.number().min(1).max(100).catch(20),
sort: z.enum(['name', 'price', 'date']).catch('name'),
order: z.enum(['asc', 'desc']).catch('asc'),
category: z.string().optional(),
search: z.string().optional(),
minPrice: z.number().min(0).optional(),
maxPrice: z.number().min(0).optional(),
})
type ProductSearch = z.infer<typeof productSearchSchema>
export const Route = createFileRoute('/products')({
validateSearch: (search) => productSearchSchema.parse(search),
component: ProductsPage,
})
function ProductsPage() {
const search = Route.useSearch()
// search: ProductSearch - fully typed with defaults
return (
<ProductList
page={search.page}
limit={search.limit}
sort={search.sort}
order={search.order}
filters={{
category: search.category,
search: search.search,
priceRange: search.minPrice && search.maxPrice
? [search.minPrice, search.maxPrice]
: undefined,
}}
/>
)
}
```
## Good Example: With Valibot
```tsx
import * as v from 'valibot'
import { valibotSearchValidator } from '@tanstack/router-valibot-adapter'
const searchSchema = v.object({
page: v.fallback(v.number(), 1),
query: v.fallback(v.string(), ''),
filters: v.fallback(
v.array(v.string()),
[]
),
})
export const Route = createFileRoute('/search')({
validateSearch: valibotSearchValidator(searchSchema),
component: SearchPage,
})
```
## Updating Search Params
```tsx
function ProductFilters() {
const navigate = useNavigate()
const search = Route.useSearch()
const updateFilters = (newFilters: Partial<ProductSearch>) => {
navigate({
to: '.', // Current route
search: (prev) => ({
...prev,
...newFilters,
page: 1, // Reset to page 1 when filters change
}),
})
}
return (
<div>
<select
value={search.sort}
onChange={(e) => updateFilters({ sort: e.target.value as ProductSearch['sort'] })}
>
<option value="name">Name</option>
<option value="price">Price</option>
<option value="date">Date</option>
</select>
</div>
)
}
```
## Context
- Search params are user input - never trust them unvalidated
- Use `.catch()` in Zod or `fallback()` in Valibot for graceful defaults
- Validation runs on every navigation - keep it fast
- Search params are inherited by child routes
- Use `search` updater function to preserve other params

View File

@@ -0,0 +1,148 @@
# split-lazy-routes: Use .lazy.tsx for Code Splitting
## Priority: MEDIUM
## Explanation
Split route components into `.lazy.tsx` files to reduce initial bundle size. The main route file keeps critical configuration (path, loaders, search validation), while lazy files contain components that load on-demand.
## Bad Example
```tsx
// routes/dashboard.tsx - Everything in one file
import { createFileRoute } from '@tanstack/react-router'
import { HeavyChartLibrary } from 'heavy-chart-library'
import { ComplexDataGrid } from 'complex-data-grid'
import { AnalyticsWidgets } from './components/AnalyticsWidgets'
export const Route = createFileRoute('/dashboard')({
loader: async ({ context }) => {
return context.queryClient.ensureQueryData(dashboardQueries.stats())
},
component: DashboardPage, // Entire component in main bundle
})
function DashboardPage() {
// Heavy components loaded even if user never visits dashboard
return (
<div>
<HeavyChartLibrary data={useLoaderData()} />
<ComplexDataGrid />
<AnalyticsWidgets />
</div>
)
}
```
## Good Example
```tsx
// routes/dashboard.tsx - Only critical config
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/dashboard')({
loader: async ({ context }) => {
return context.queryClient.ensureQueryData(dashboardQueries.stats())
},
// No component - it's in the lazy file
})
// routes/dashboard.lazy.tsx - Lazy-loaded component
import { createLazyFileRoute } from '@tanstack/react-router'
import { HeavyChartLibrary } from 'heavy-chart-library'
import { ComplexDataGrid } from 'complex-data-grid'
import { AnalyticsWidgets } from './components/AnalyticsWidgets'
export const Route = createLazyFileRoute('/dashboard')({
component: DashboardPage,
pendingComponent: DashboardSkeleton,
errorComponent: DashboardError,
})
function DashboardPage() {
const data = Route.useLoaderData()
return (
<div>
<HeavyChartLibrary data={data} />
<ComplexDataGrid />
<AnalyticsWidgets />
</div>
)
}
function DashboardSkeleton() {
return <div className="dashboard-skeleton">Loading dashboard...</div>
}
function DashboardError({ error }: { error: Error }) {
return <div>Failed to load dashboard: {error.message}</div>
}
```
## What Goes Where
```tsx
// Main route file (routes/example.tsx)
// - path configuration (implicit from file location)
// - validateSearch
// - beforeLoad (auth checks, redirects)
// - loader (data fetching)
// - loaderDeps
// - context manipulation
// - Static route data
// Lazy file (routes/example.lazy.tsx)
// - component
// - pendingComponent
// - errorComponent
// - notFoundComponent
```
## Using getRouteApi in Lazy Components
```tsx
// routes/posts/$postId.lazy.tsx
import { createLazyFileRoute, getRouteApi } from '@tanstack/react-router'
const route = getRouteApi('/posts/$postId')
export const Route = createLazyFileRoute('/posts/$postId')({
component: PostPage,
})
function PostPage() {
// Type-safe access without importing main route file
const { postId } = route.useParams()
const data = route.useLoaderData()
return <article>{/* ... */}</article>
}
```
## Automatic Code Splitting
```tsx
// vite.config.ts - Enable automatic splitting
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
export default defineConfig({
plugins: [
TanStackRouterVite({
autoCodeSplitting: true, // Automatically splits all route components
}),
react(),
],
})
// With autoCodeSplitting, you don't need .lazy.tsx files
// The plugin handles the splitting automatically
```
## Context
- Lazy loading reduces initial bundle size significantly
- Loaders are NOT lazy - they need to run before rendering
- `createLazyFileRoute` only accepts component-related options
- Use `getRouteApi()` for type-safe hook access in lazy files
- Consider `autoCodeSplitting: true` for simpler setup
- Virtual routes auto-generate when only .lazy.tsx exists

View File

@@ -0,0 +1,113 @@
# ts-register-router: Register Router Type for Global Inference
## Priority: CRITICAL
## Explanation
Register your router instance with TypeScript's module declaration to enable type inference across your entire application. Without registration, hooks like `useNavigate`, `useParams`, and `useSearch` won't know your route structure.
## Bad Example
```tsx
// router.tsx - Missing type registration
import { createRouter, createRootRoute } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
export const router = createRouter({ routeTree })
// components/Navigation.tsx
import { useNavigate } from '@tanstack/react-router'
function Navigation() {
const navigate = useNavigate()
// TypeScript doesn't know valid routes - no autocomplete or type checking
navigate({ to: '/posts/$postId' }) // No error even if route doesn't exist
}
```
## Good Example
```tsx
// router.tsx
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
export const router = createRouter({ routeTree })
// Register the router instance for type inference
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
// components/Navigation.tsx
import { useNavigate } from '@tanstack/react-router'
function Navigation() {
const navigate = useNavigate()
// Full type safety - TypeScript knows all valid routes
navigate({ to: '/posts/$postId', params: { postId: '123' } })
// Type error if route doesn't exist
navigate({ to: '/invalid-route' }) // Error: Type '"/invalid-route"' is not assignable...
// Autocomplete for params
navigate({
to: '/users/$userId/posts/$postId',
params: { userId: '1', postId: '2' }, // Both required
})
}
```
## Benefits of Registration
```tsx
// After registration, all these get full type inference:
// 1. Navigation
const navigate = useNavigate()
navigate({ to: '/posts/$postId', params: { postId: '123' } })
// 2. Link component
<Link to="/posts/$postId" params={{ postId: '123' }}>View Post</Link>
// 3. useParams hook
const { postId } = useParams({ from: '/posts/$postId' }) // postId: string
// 4. useSearch hook
const search = useSearch({ from: '/posts' }) // Knows search param types
// 5. useLoaderData hook
const data = useLoaderData({ from: '/posts/$postId' }) // Knows loader return type
```
## File-Based Routing Setup
```tsx
// With file-based routing, routeTree is auto-generated
// router.tsx
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen' // Generated file
export const router = createRouter({
routeTree,
defaultPreload: 'intent',
})
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
```
## Context
- Must be done once, typically in your router configuration file
- Enables IDE autocomplete for routes, params, and search params
- Catches invalid routes at compile time
- Works with both file-based and code-based routing
- Required for full TypeScript benefits of TanStack Router

View File

@@ -0,0 +1,130 @@
# ts-use-from-param: Use `from` Parameter for Type Narrowing
## Priority: CRITICAL
## Explanation
When using hooks like `useParams`, `useSearch`, or `useLoaderData`, provide the `from` parameter to get exact types for that route. Without it, TypeScript returns a union of all possible types across all routes.
## Bad Example
```tsx
// Without 'from' - TypeScript doesn't know which route's types to use
function PostDetail() {
// params could be from ANY route - types are unioned
const params = useParams()
// params: { postId?: string; userId?: string; categoryId?: string; ... }
// TypeScript can't guarantee postId exists
console.log(params.postId) // postId: string | undefined
}
// Similarly for search params
function SearchResults() {
const search = useSearch()
// search: union of ALL routes' search params
}
```
## Good Example
```tsx
// With 'from' - exact types for this specific route
function PostDetail() {
const params = useParams({ from: '/posts/$postId' })
// params: { postId: string } - exactly what this route provides
console.log(params.postId) // postId: string (guaranteed)
}
// Full path matching
function UserPost() {
const params = useParams({ from: '/users/$userId/posts/$postId' })
// params: { userId: string; postId: string }
}
// Search params with type narrowing
function SearchResults() {
const search = useSearch({ from: '/search' })
// search: exactly the validated search params for /search route
}
// Loader data with type inference
function PostPage() {
const { post, comments } = useLoaderData({ from: '/posts/$postId' })
// Exact types from your loader function
}
```
## Using Route.fullPath for Type Safety
```tsx
// routes/posts/$postId.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
const post = await fetchPost(params.postId)
return { post }
},
component: PostComponent,
})
function PostComponent() {
// Use Route.fullPath for guaranteed type matching
const params = useParams({ from: Route.fullPath })
const { post } = useLoaderData({ from: Route.fullPath })
// Or use route-specific helper (preferred in same file)
const { postId } = Route.useParams()
const data = Route.useLoaderData()
}
```
## Using getRouteApi for Code-Split Components
```tsx
// components/PostDetail.tsx (separate file from route)
import { getRouteApi } from '@tanstack/react-router'
// Get type-safe access without importing the route
const postRoute = getRouteApi('/posts/$postId')
export function PostDetail() {
const params = postRoute.useParams()
// params: { postId: string }
const data = postRoute.useLoaderData()
// data: exact loader return type
const search = postRoute.useSearch()
// search: exact search param types
}
```
## When to Use strict: false
```tsx
// In shared components that work across multiple routes
function Breadcrumbs() {
// strict: false returns union types but allows component reuse
const params = useParams({ strict: false })
const location = useLocation()
// params may or may not have certain values
return (
<nav>
{params.userId && <span>User: {params.userId}</span>}
{params.postId && <span>Post: {params.postId}</span>}
</nav>
)
}
```
## Context
- Always use `from` in route-specific components for exact types
- Use `Route.useParams()` / `Route.useLoaderData()` within route files
- Use `getRouteApi()` in components split from route files
- Use `strict: false` only in truly generic, cross-route components
- The `from` path must match exactly (including params like `$postId`)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,123 @@
# React Best Practices
A structured repository for creating and maintaining React Best Practices optimized for agents and LLMs.
## Structure
- `rules/` - Individual rule files (one per rule)
- `_sections.md` - Section metadata (titles, impacts, descriptions)
- `_template.md` - Template for creating new rules
- `area-description.md` - Individual rule files
- `src/` - Build scripts and utilities
- `metadata.json` - Document metadata (version, organization, abstract)
- __`AGENTS.md`__ - Compiled output (generated)
- __`test-cases.json`__ - Test cases for LLM evaluation (generated)
## Getting Started
1. Install dependencies:
```bash
pnpm install
```
2. Build AGENTS.md from rules:
```bash
pnpm build
```
3. Validate rule files:
```bash
pnpm validate
```
4. Extract test cases:
```bash
pnpm extract-tests
```
## Creating a New Rule
1. Copy `rules/_template.md` to `rules/area-description.md`
2. Choose the appropriate area prefix:
- `async-` for Eliminating Waterfalls (Section 1)
- `bundle-` for Bundle Size Optimization (Section 2)
- `server-` for Server-Side Performance (Section 3)
- `client-` for Client-Side Data Fetching (Section 4)
- `rerender-` for Re-render Optimization (Section 5)
- `rendering-` for Rendering Performance (Section 6)
- `js-` for JavaScript Performance (Section 7)
- `advanced-` for Advanced Patterns (Section 8)
3. Fill in the frontmatter and content
4. Ensure you have clear examples with explanations
5. Run `pnpm build` to regenerate AGENTS.md and test-cases.json
## Rule File Structure
Each rule file should follow this structure:
```markdown
---
title: Rule Title Here
impact: MEDIUM
impactDescription: Optional description
tags: tag1, tag2, tag3
---
## Rule Title Here
Brief explanation of the rule and why it matters.
**Incorrect (description of what's wrong):**
```typescript
// Bad code example
```
**Correct (description of what's right):**
```typescript
// Good code example
```
Optional explanatory text after examples.
Reference: [Link](https://example.com)
## File Naming Convention
- Files starting with `_` are special (excluded from build)
- Rule files: `area-description.md` (e.g., `async-parallel.md`)
- Section is automatically inferred from filename prefix
- Rules are sorted alphabetically by title within each section
- IDs (e.g., 1.1, 1.2) are auto-generated during build
## Impact Levels
- `CRITICAL` - Highest priority, major performance gains
- `HIGH` - Significant performance improvements
- `MEDIUM-HIGH` - Moderate-high gains
- `MEDIUM` - Moderate performance improvements
- `LOW-MEDIUM` - Low-medium gains
- `LOW` - Incremental improvements
## Scripts
- `pnpm build` - Compile rules into AGENTS.md
- `pnpm validate` - Validate all rule files
- `pnpm extract-tests` - Extract test cases for LLM evaluation
- `pnpm dev` - Build and validate
## Contributing
When adding or modifying rules:
1. Use the correct filename prefix for your section
2. Follow the `_template.md` structure
3. Include clear bad/good examples with explanations
4. Add appropriate tags
5. Run `pnpm build` to regenerate AGENTS.md and test-cases.json
6. Rules are automatically sorted by title - no need to manage numbers!
## Acknowledgments
Originally created by [@shuding](https://x.com/shuding) at [Vercel](https://vercel.com).

View File

@@ -0,0 +1,146 @@
---
name: vercel-react-best-practices
description: React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.
license: MIT
metadata:
author: vercel
version: "1.0.0"
---
# Vercel React Best Practices
Comprehensive performance optimization guide for React and Next.js applications, maintained by Vercel. Contains 67 rules across 8 categories, prioritized by impact to guide automated refactoring and code generation.
## When to Apply
Reference these guidelines when:
- Writing new React components or Next.js pages
- Implementing data fetching (client or server-side)
- Reviewing code for performance issues
- Refactoring existing React/Next.js code
- Optimizing bundle size or load times
## Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|----------|----------|--------|--------|
| 1 | Eliminating Waterfalls | CRITICAL | `async-` |
| 2 | Bundle Size Optimization | CRITICAL | `bundle-` |
| 3 | Server-Side Performance | HIGH | `server-` |
| 4 | Client-Side Data Fetching | MEDIUM-HIGH | `client-` |
| 5 | Re-render Optimization | MEDIUM | `rerender-` |
| 6 | Rendering Performance | MEDIUM | `rendering-` |
| 7 | JavaScript Performance | LOW-MEDIUM | `js-` |
| 8 | Advanced Patterns | LOW | `advanced-` |
## Quick Reference
### 1. Eliminating Waterfalls (CRITICAL)
- `async-cheap-condition-before-await` - Check cheap sync conditions before awaiting flags or remote values
- `async-defer-await` - Move await into branches where actually used
- `async-parallel` - Use Promise.all() for independent operations
- `async-dependencies` - Use better-all for partial dependencies
- `async-api-routes` - Start promises early, await late in API routes
- `async-suspense-boundaries` - Use Suspense to stream content
### 2. Bundle Size Optimization (CRITICAL)
- `bundle-barrel-imports` - Import directly, avoid barrel files
- `bundle-dynamic-imports` - Use next/dynamic for heavy components
- `bundle-defer-third-party` - Load analytics/logging after hydration
- `bundle-conditional` - Load modules only when feature is activated
- `bundle-preload` - Preload on hover/focus for perceived speed
### 3. Server-Side Performance (HIGH)
- `server-auth-actions` - Authenticate server actions like API routes
- `server-cache-react` - Use React.cache() for per-request deduplication
- `server-cache-lru` - Use LRU cache for cross-request caching
- `server-dedup-props` - Avoid duplicate serialization in RSC props
- `server-hoist-static-io` - Hoist static I/O (fonts, logos) to module level
- `server-serialization` - Minimize data passed to client components
- `server-parallel-fetching` - Restructure components to parallelize fetches
- `server-parallel-nested-fetching` - Chain nested fetches per item in Promise.all
- `server-after-nonblocking` - Use after() for non-blocking operations
### 4. Client-Side Data Fetching (MEDIUM-HIGH)
- `client-swr-dedup` - Use SWR for automatic request deduplication
- `client-event-listeners` - Deduplicate global event listeners
- `client-passive-event-listeners` - Use passive listeners for scroll
- `client-localstorage-schema` - Version and minimize localStorage data
### 5. Re-render Optimization (MEDIUM)
- `rerender-defer-reads` - Don't subscribe to state only used in callbacks
- `rerender-memo` - Extract expensive work into memoized components
- `rerender-memo-with-default-value` - Hoist default non-primitive props
- `rerender-dependencies` - Use primitive dependencies in effects
- `rerender-derived-state` - Subscribe to derived booleans, not raw values
- `rerender-derived-state-no-effect` - Derive state during render, not effects
- `rerender-functional-setstate` - Use functional setState for stable callbacks
- `rerender-lazy-state-init` - Pass function to useState for expensive values
- `rerender-simple-expression-in-memo` - Avoid memo for simple primitives
- `rerender-split-combined-hooks` - Split hooks with independent dependencies
- `rerender-move-effect-to-event` - Put interaction logic in event handlers
- `rerender-transitions` - Use startTransition for non-urgent updates
- `rerender-use-deferred-value` - Defer expensive renders to keep input responsive
- `rerender-use-ref-transient-values` - Use refs for transient frequent values
- `rerender-no-inline-components` - Don't define components inside components
### 6. Rendering Performance (MEDIUM)
- `rendering-animate-svg-wrapper` - Animate div wrapper, not SVG element
- `rendering-content-visibility` - Use content-visibility for long lists
- `rendering-hoist-jsx` - Extract static JSX outside components
- `rendering-svg-precision` - Reduce SVG coordinate precision
- `rendering-hydration-no-flicker` - Use inline script for client-only data
- `rendering-hydration-suppress-warning` - Suppress expected mismatches
- `rendering-activity` - Use Activity component for show/hide
- `rendering-conditional-render` - Use ternary, not && for conditionals
- `rendering-usetransition-loading` - Prefer useTransition for loading state
- `rendering-resource-hints` - Use React DOM resource hints for preloading
- `rendering-script-defer-async` - Use defer or async on script tags
### 7. JavaScript Performance (LOW-MEDIUM)
- `js-batch-dom-css` - Group CSS changes via classes or cssText
- `js-index-maps` - Build Map for repeated lookups
- `js-cache-property-access` - Cache object properties in loops
- `js-cache-function-results` - Cache function results in module-level Map
- `js-cache-storage` - Cache localStorage/sessionStorage reads
- `js-combine-iterations` - Combine multiple filter/map into one loop
- `js-length-check-first` - Check array length before expensive comparison
- `js-early-exit` - Return early from functions
- `js-hoist-regexp` - Hoist RegExp creation outside loops
- `js-min-max-loop` - Use loop for min/max instead of sort
- `js-set-map-lookups` - Use Set/Map for O(1) lookups
- `js-tosorted-immutable` - Use toSorted() for immutability
- `js-flatmap-filter` - Use flatMap to map and filter in one pass
- `js-request-idle-callback` - Defer non-critical work to browser idle time
### 8. Advanced Patterns (LOW)
- `advanced-event-handler-refs` - Store event handlers in refs
- `advanced-init-once` - Initialize app once per app load
- `advanced-use-latest` - useLatest for stable callback refs
## How to Use
Read individual rule files for detailed explanations and code examples:
```
rules/async-parallel.md
rules/bundle-barrel-imports.md
```
Each rule file contains:
- Brief explanation of why it matters
- Incorrect code example with explanation
- Correct code example with explanation
- Additional context and references
## Full Compiled Document
For the complete guide with all rules expanded: `AGENTS.md`

View File

@@ -0,0 +1,46 @@
# Sections
This file defines all sections, their ordering, impact levels, and descriptions.
The section ID (in parentheses) is the filename prefix used to group rules.
---
## 1. Eliminating Waterfalls (async)
**Impact:** CRITICAL
**Description:** Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains.
## 2. Bundle Size Optimization (bundle)
**Impact:** CRITICAL
**Description:** Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint.
## 3. Server-Side Performance (server)
**Impact:** HIGH
**Description:** Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times.
## 4. Client-Side Data Fetching (client)
**Impact:** MEDIUM-HIGH
**Description:** Automatic deduplication and efficient data fetching patterns reduce redundant network requests.
## 5. Re-render Optimization (rerender)
**Impact:** MEDIUM
**Description:** Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness.
## 6. Rendering Performance (rendering)
**Impact:** MEDIUM
**Description:** Optimizing the rendering process reduces the work the browser needs to do.
## 7. JavaScript Performance (js)
**Impact:** LOW-MEDIUM
**Description:** Micro-optimizations for hot paths can add up to meaningful improvements.
## 8. Advanced Patterns (advanced)
**Impact:** LOW
**Description:** Advanced patterns for specific cases that require careful implementation.

View File

@@ -0,0 +1,28 @@
---
title: Rule Title Here
impact: MEDIUM
impactDescription: Optional description of impact (e.g., "20-50% improvement")
tags: tag1, tag2
---
## Rule Title Here
**Impact: MEDIUM (optional impact description)**
Brief explanation of the rule and why it matters. This should be clear and concise, explaining the performance implications.
**Incorrect (description of what's wrong):**
```typescript
// Bad code example here
const bad = example()
```
**Correct (description of what's right):**
```typescript
// Good code example here
const good = example()
```
Reference: [Link to documentation or resource](https://example.com)

View File

@@ -0,0 +1,55 @@
---
title: Store Event Handlers in Refs
impact: LOW
impactDescription: stable subscriptions
tags: advanced, hooks, refs, event-handlers, optimization
---
## Store Event Handlers in Refs
Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.
**Incorrect (re-subscribes on every render):**
```tsx
function useWindowEvent(event: string, handler: (e) => void) {
useEffect(() => {
window.addEventListener(event, handler)
return () => window.removeEventListener(event, handler)
}, [event, handler])
}
```
**Correct (stable subscription):**
```tsx
function useWindowEvent(event: string, handler: (e) => void) {
const handlerRef = useRef(handler)
useEffect(() => {
handlerRef.current = handler
}, [handler])
useEffect(() => {
const listener = (e) => handlerRef.current(e)
window.addEventListener(event, listener)
return () => window.removeEventListener(event, listener)
}, [event])
}
```
**Alternative: use `useEffectEvent` if you're on latest React:**
```tsx
import { useEffectEvent } from 'react'
function useWindowEvent(event: string, handler: (e) => void) {
const onEvent = useEffectEvent(handler)
useEffect(() => {
window.addEventListener(event, onEvent)
return () => window.removeEventListener(event, onEvent)
}, [event])
}
```
`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.

View File

@@ -0,0 +1,42 @@
---
title: Initialize App Once, Not Per Mount
impact: LOW-MEDIUM
impactDescription: avoids duplicate init in development
tags: initialization, useEffect, app-startup, side-effects
---
## Initialize App Once, Not Per Mount
Do not put app-wide initialization that must run once per app load inside `useEffect([])` of a component. Components can remount and effects will re-run. Use a module-level guard or top-level init in the entry module instead.
**Incorrect (runs twice in dev, re-runs on remount):**
```tsx
function Comp() {
useEffect(() => {
loadFromStorage()
checkAuthToken()
}, [])
// ...
}
```
**Correct (once per app load):**
```tsx
let didInit = false
function Comp() {
useEffect(() => {
if (didInit) return
didInit = true
loadFromStorage()
checkAuthToken()
}, [])
// ...
}
```
Reference: [Initializing the application](https://react.dev/learn/you-might-not-need-an-effect#initializing-the-application)

View File

@@ -0,0 +1,39 @@
---
title: useEffectEvent for Stable Callback Refs
impact: LOW
impactDescription: prevents effect re-runs
tags: advanced, hooks, useEffectEvent, refs, optimization
---
## useEffectEvent for Stable Callback Refs
Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.
**Incorrect (effect re-runs on every callback change):**
```tsx
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
const [query, setQuery] = useState('')
useEffect(() => {
const timeout = setTimeout(() => onSearch(query), 300)
return () => clearTimeout(timeout)
}, [query, onSearch])
}
```
**Correct (using React's useEffectEvent):**
```tsx
import { useEffectEvent } from 'react';
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
const [query, setQuery] = useState('')
const onSearchEvent = useEffectEvent(onSearch)
useEffect(() => {
const timeout = setTimeout(() => onSearchEvent(query), 300)
return () => clearTimeout(timeout)
}, [query])
}
```

View File

@@ -0,0 +1,38 @@
---
title: Prevent Waterfall Chains in API Routes
impact: CRITICAL
impactDescription: 2-10× improvement
tags: api-routes, server-actions, waterfalls, parallelization
---
## Prevent Waterfall Chains in API Routes
In API routes and Server Actions, start independent operations immediately, even if you don't await them yet.
**Incorrect (config waits for auth, data waits for both):**
```typescript
export async function GET(request: Request) {
const session = await auth()
const config = await fetchConfig()
const data = await fetchData(session.user.id)
return Response.json({ data, config })
}
```
**Correct (auth and config start immediately):**
```typescript
export async function GET(request: Request) {
const sessionPromise = auth()
const configPromise = fetchConfig()
const session = await sessionPromise
const [config, data] = await Promise.all([
configPromise,
fetchData(session.user.id)
])
return Response.json({ data, config })
}
```
For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization).

View File

@@ -0,0 +1,37 @@
---
title: Check Cheap Conditions Before Async Flags
impact: HIGH
impactDescription: avoids unnecessary async work when a synchronous guard already fails
tags: async, await, feature-flags, short-circuit, conditional
---
## Check Cheap Conditions Before Async Flags
When a branch uses `await` for a flag or remote value and also requires a **cheap synchronous** condition (local props, request metadata, already-loaded state), evaluate the cheap condition **first**. Otherwise you pay for the async call even when the compound condition can never be true.
This is a specialization of [Defer Await Until Needed](./async-defer-await.md) for `flag && cheapCondition` style checks.
**Incorrect:**
```typescript
const someFlag = await getFlag()
if (someFlag && someCondition) {
// ...
}
```
**Correct:**
```typescript
if (someCondition) {
const someFlag = await getFlag()
if (someFlag) {
// ...
}
}
```
This matters when `getFlag` hits the network, a feature-flag service, or `React.cache` / DB work: skipping it when `someCondition` is false removes that cost on the cold path.
Keep the original order if `someCondition` is expensive, depends on the flag, or you must run side effects in a fixed order.

View File

@@ -0,0 +1,82 @@
---
title: Defer Await Until Needed
impact: HIGH
impactDescription: avoids blocking unused code paths
tags: async, await, conditional, optimization
---
## Defer Await Until Needed
Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them.
**Incorrect (blocks both branches):**
```typescript
async function handleRequest(userId: string, skipProcessing: boolean) {
const userData = await fetchUserData(userId)
if (skipProcessing) {
// Returns immediately but still waited for userData
return { skipped: true }
}
// Only this branch uses userData
return processUserData(userData)
}
```
**Correct (only blocks when needed):**
```typescript
async function handleRequest(userId: string, skipProcessing: boolean) {
if (skipProcessing) {
// Returns immediately without waiting
return { skipped: true }
}
// Fetch only when needed
const userData = await fetchUserData(userId)
return processUserData(userData)
}
```
**Another example (early return optimization):**
```typescript
// Incorrect: always fetches permissions
async function updateResource(resourceId: string, userId: string) {
const permissions = await fetchPermissions(userId)
const resource = await getResource(resourceId)
if (!resource) {
return { error: 'Not found' }
}
if (!permissions.canEdit) {
return { error: 'Forbidden' }
}
return await updateResourceData(resource, permissions)
}
// Correct: fetches only when needed
async function updateResource(resourceId: string, userId: string) {
const resource = await getResource(resourceId)
if (!resource) {
return { error: 'Not found' }
}
const permissions = await fetchPermissions(userId)
if (!permissions.canEdit) {
return { error: 'Forbidden' }
}
return await updateResourceData(resource, permissions)
}
```
This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.
For `await getFlag()` combined with a cheap synchronous guard (`flag && someCondition`), see [Check Cheap Conditions Before Async Flags](./async-cheap-condition-before-await.md).

View File

@@ -0,0 +1,51 @@
---
title: Dependency-Based Parallelization
impact: CRITICAL
impactDescription: 2-10× improvement
tags: async, parallelization, dependencies, better-all
---
## Dependency-Based Parallelization
For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment.
**Incorrect (profile waits for config unnecessarily):**
```typescript
const [user, config] = await Promise.all([
fetchUser(),
fetchConfig()
])
const profile = await fetchProfile(user.id)
```
**Correct (config and profile run in parallel):**
```typescript
import { all } from 'better-all'
const { user, config, profile } = await all({
async user() { return fetchUser() },
async config() { return fetchConfig() },
async profile() {
return fetchProfile((await this.$.user).id)
}
})
```
**Alternative without extra dependencies:**
We can also create all the promises first, and do `Promise.all()` at the end.
```typescript
const userPromise = fetchUser()
const profilePromise = userPromise.then(user => fetchProfile(user.id))
const [user, config, profile] = await Promise.all([
userPromise,
fetchConfig(),
profilePromise
])
```
Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)

Some files were not shown because too many files have changed in this diff Show More