@malamute/ai-rules 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/README.md +270 -121
  2. package/bin/cli.js +5 -2
  3. package/configs/_shared/.claude/rules/conventions/documentation.md +324 -0
  4. package/configs/_shared/.claude/rules/conventions/git.md +265 -0
  5. package/configs/_shared/.claude/rules/{performance.md → conventions/performance.md} +1 -1
  6. package/configs/_shared/.claude/rules/conventions/principles.md +334 -0
  7. package/configs/_shared/.claude/rules/devops/ci-cd.md +262 -0
  8. package/configs/_shared/.claude/rules/devops/docker.md +275 -0
  9. package/configs/_shared/.claude/rules/devops/nx.md +194 -0
  10. package/configs/_shared/.claude/rules/domain/backend/api-design.md +203 -0
  11. package/configs/_shared/.claude/rules/lang/csharp/async.md +220 -0
  12. package/configs/_shared/.claude/rules/lang/csharp/csharp.md +314 -0
  13. package/configs/_shared/.claude/rules/lang/csharp/linq.md +210 -0
  14. package/configs/_shared/.claude/rules/lang/python/async.md +337 -0
  15. package/configs/_shared/.claude/rules/lang/python/celery.md +476 -0
  16. package/configs/_shared/.claude/rules/lang/python/config.md +339 -0
  17. package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/database/sqlalchemy.md +6 -1
  18. package/configs/_shared/.claude/rules/lang/python/deployment.md +523 -0
  19. package/configs/_shared/.claude/rules/lang/python/error-handling.md +330 -0
  20. package/configs/_shared/.claude/rules/lang/python/migrations.md +421 -0
  21. package/configs/_shared/.claude/rules/lang/python/python.md +172 -0
  22. package/configs/_shared/.claude/rules/lang/python/repository.md +383 -0
  23. package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/testing.md +2 -69
  24. package/configs/_shared/.claude/rules/lang/typescript/async.md +447 -0
  25. package/configs/_shared/.claude/rules/lang/typescript/generics.md +356 -0
  26. package/configs/_shared/.claude/rules/lang/typescript/typescript.md +212 -0
  27. package/configs/_shared/.claude/rules/quality/error-handling.md +48 -0
  28. package/configs/_shared/.claude/rules/quality/logging.md +45 -0
  29. package/configs/_shared/.claude/rules/quality/observability.md +240 -0
  30. package/configs/_shared/.claude/rules/quality/testing-patterns.md +65 -0
  31. package/configs/_shared/.claude/rules/security/secrets-management.md +222 -0
  32. package/configs/_shared/.claude/skills/analysis/explore/SKILL.md +257 -0
  33. package/configs/_shared/.claude/skills/analysis/security-audit/SKILL.md +184 -0
  34. package/configs/_shared/.claude/skills/dev/api-endpoint/SKILL.md +126 -0
  35. package/configs/_shared/.claude/{commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
  36. package/configs/_shared/.claude/{commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
  37. package/configs/_shared/.claude/{commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
  38. package/configs/_shared/.claude/skills/infra/deploy/SKILL.md +139 -0
  39. package/configs/_shared/.claude/skills/infra/docker/SKILL.md +95 -0
  40. package/configs/_shared/.claude/skills/infra/migration/SKILL.md +158 -0
  41. package/configs/_shared/.claude/skills/nx/nx-affected/SKILL.md +72 -0
  42. package/configs/_shared/.claude/skills/nx/nx-lib/SKILL.md +375 -0
  43. package/configs/_shared/CLAUDE.md +52 -149
  44. package/configs/angular/.claude/rules/{components.md → core/components.md} +69 -15
  45. package/configs/angular/.claude/rules/core/resource.md +285 -0
  46. package/configs/angular/.claude/rules/core/signals.md +323 -0
  47. package/configs/angular/.claude/rules/http.md +338 -0
  48. package/configs/angular/.claude/rules/routing.md +291 -0
  49. package/configs/angular/.claude/rules/ssr.md +312 -0
  50. package/configs/angular/.claude/rules/state/signal-store.md +408 -0
  51. package/configs/angular/.claude/rules/{state.md → state/state.md} +2 -2
  52. package/configs/angular/.claude/rules/testing.md +7 -7
  53. package/configs/angular/.claude/rules/ui/aria.md +422 -0
  54. package/configs/angular/.claude/rules/ui/forms.md +424 -0
  55. package/configs/angular/.claude/rules/ui/pipes-directives.md +335 -0
  56. package/configs/angular/.claude/settings.json +1 -0
  57. package/configs/angular/.claude/skills/ngrx-slice/SKILL.md +362 -0
  58. package/configs/angular/.claude/skills/signal-store/SKILL.md +445 -0
  59. package/configs/angular/CLAUDE.md +24 -216
  60. package/configs/dotnet/.claude/rules/background-services.md +552 -0
  61. package/configs/dotnet/.claude/rules/configuration.md +426 -0
  62. package/configs/dotnet/.claude/rules/ddd.md +447 -0
  63. package/configs/dotnet/.claude/rules/dependency-injection.md +343 -0
  64. package/configs/dotnet/.claude/rules/mediatr.md +320 -0
  65. package/configs/dotnet/.claude/rules/middleware.md +489 -0
  66. package/configs/dotnet/.claude/rules/result-pattern.md +363 -0
  67. package/configs/dotnet/.claude/rules/validation.md +388 -0
  68. package/configs/dotnet/.claude/settings.json +21 -3
  69. package/configs/dotnet/CLAUDE.md +53 -286
  70. package/configs/fastapi/.claude/rules/background-tasks.md +254 -0
  71. package/configs/fastapi/.claude/rules/dependencies.md +170 -0
  72. package/configs/{python → fastapi}/.claude/rules/fastapi.md +61 -1
  73. package/configs/fastapi/.claude/rules/lifespan.md +274 -0
  74. package/configs/fastapi/.claude/rules/middleware.md +229 -0
  75. package/configs/fastapi/.claude/rules/pydantic.md +433 -0
  76. package/configs/fastapi/.claude/rules/responses.md +251 -0
  77. package/configs/fastapi/.claude/rules/routers.md +202 -0
  78. package/configs/fastapi/.claude/rules/security.md +222 -0
  79. package/configs/fastapi/.claude/rules/testing.md +251 -0
  80. package/configs/fastapi/.claude/rules/websockets.md +298 -0
  81. package/configs/fastapi/.claude/settings.json +33 -0
  82. package/configs/fastapi/CLAUDE.md +144 -0
  83. package/configs/flask/.claude/rules/blueprints.md +208 -0
  84. package/configs/flask/.claude/rules/cli.md +285 -0
  85. package/configs/flask/.claude/rules/configuration.md +281 -0
  86. package/configs/flask/.claude/rules/context.md +238 -0
  87. package/configs/flask/.claude/rules/error-handlers.md +278 -0
  88. package/configs/flask/.claude/rules/extensions.md +278 -0
  89. package/configs/flask/.claude/rules/flask.md +171 -0
  90. package/configs/flask/.claude/rules/marshmallow.md +206 -0
  91. package/configs/flask/.claude/rules/security.md +267 -0
  92. package/configs/flask/.claude/rules/testing.md +284 -0
  93. package/configs/flask/.claude/settings.json +33 -0
  94. package/configs/flask/CLAUDE.md +166 -0
  95. package/configs/nestjs/.claude/rules/common-patterns.md +300 -0
  96. package/configs/nestjs/.claude/rules/filters.md +376 -0
  97. package/configs/nestjs/.claude/rules/interceptors.md +317 -0
  98. package/configs/nestjs/.claude/rules/middleware.md +321 -0
  99. package/configs/nestjs/.claude/rules/modules.md +26 -0
  100. package/configs/nestjs/.claude/rules/pipes.md +351 -0
  101. package/configs/nestjs/.claude/rules/websockets.md +451 -0
  102. package/configs/nestjs/.claude/settings.json +16 -2
  103. package/configs/nestjs/CLAUDE.md +57 -215
  104. package/configs/nextjs/.claude/rules/api-routes.md +358 -0
  105. package/configs/nextjs/.claude/rules/authentication.md +355 -0
  106. package/configs/nextjs/.claude/rules/components.md +52 -0
  107. package/configs/nextjs/.claude/rules/data-fetching.md +249 -0
  108. package/configs/nextjs/.claude/rules/database.md +400 -0
  109. package/configs/nextjs/.claude/rules/middleware.md +303 -0
  110. package/configs/nextjs/.claude/rules/routing.md +324 -0
  111. package/configs/nextjs/.claude/rules/seo.md +350 -0
  112. package/configs/nextjs/.claude/rules/server-actions.md +353 -0
  113. package/configs/nextjs/.claude/rules/state/zustand.md +6 -6
  114. package/configs/nextjs/.claude/settings.json +5 -0
  115. package/configs/nextjs/CLAUDE.md +69 -331
  116. package/package.json +23 -9
  117. package/src/cli.js +220 -0
  118. package/src/config.js +29 -0
  119. package/src/index.js +13 -0
  120. package/src/installer.js +361 -0
  121. package/src/merge.js +116 -0
  122. package/src/tech-config.json +29 -0
  123. package/src/utils.js +96 -0
  124. package/configs/python/.claude/rules/flask.md +0 -332
  125. package/configs/python/.claude/settings.json +0 -18
  126. package/configs/python/CLAUDE.md +0 -273
  127. package/src/install.js +0 -315
  128. /package/configs/_shared/.claude/rules/{accessibility.md → domain/frontend/accessibility.md} +0 -0
  129. /package/configs/_shared/.claude/rules/{security.md → security/security.md} +0 -0
  130. /package/configs/_shared/.claude/skills/{debug → dev/debug}/SKILL.md +0 -0
  131. /package/configs/_shared/.claude/skills/{learning → dev/learning}/SKILL.md +0 -0
  132. /package/configs/_shared/.claude/skills/{spec → dev/spec}/SKILL.md +0 -0
  133. /package/configs/_shared/.claude/skills/{review → git/review}/SKILL.md +0 -0
@@ -0,0 +1,400 @@
1
+ ---
2
+ paths:
3
+ - "**/prisma/**"
4
+ - "**/db/**"
5
+ - "**/lib/db.ts"
6
+ - "**/lib/prisma.ts"
7
+ ---
8
+
9
+ # Next.js Database (Prisma)
10
+
11
+ ## Prisma Setup
12
+
13
+ ### Client Singleton
14
+
15
+ ```typescript
16
+ // lib/prisma.ts
17
+ import { PrismaClient } from '@prisma/client';
18
+
19
+ const globalForPrisma = globalThis as unknown as {
20
+ prisma: PrismaClient | undefined;
21
+ };
22
+
23
+ export const prisma = globalForPrisma.prisma ?? new PrismaClient({
24
+ log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
25
+ });
26
+
27
+ if (process.env.NODE_ENV !== 'production') {
28
+ globalForPrisma.prisma = prisma;
29
+ }
30
+ ```
31
+
32
+ ### Schema
33
+
34
+ ```prisma
35
+ // prisma/schema.prisma
36
+ generator client {
37
+ provider = "prisma-client-js"
38
+ }
39
+
40
+ datasource db {
41
+ provider = "postgresql"
42
+ url = env("DATABASE_URL")
43
+ }
44
+
45
+ model User {
46
+ id String @id @default(cuid())
47
+ email String @unique
48
+ name String?
49
+ role Role @default(USER)
50
+ posts Post[]
51
+ createdAt DateTime @default(now())
52
+ updatedAt DateTime @updatedAt
53
+
54
+ @@index([email])
55
+ }
56
+
57
+ model Post {
58
+ id String @id @default(cuid())
59
+ title String
60
+ slug String @unique
61
+ content String?
62
+ published Boolean @default(false)
63
+ author User @relation(fields: [authorId], references: [id])
64
+ authorId String
65
+ createdAt DateTime @default(now())
66
+ updatedAt DateTime @updatedAt
67
+
68
+ @@index([authorId])
69
+ @@index([slug])
70
+ }
71
+
72
+ enum Role {
73
+ USER
74
+ ADMIN
75
+ }
76
+ ```
77
+
78
+ ## Query Patterns
79
+
80
+ ### Server Components
81
+
82
+ ```typescript
83
+ // app/users/page.tsx
84
+ import { prisma } from '@/lib/prisma';
85
+
86
+ export default async function UsersPage() {
87
+ const users = await prisma.user.findMany({
88
+ select: {
89
+ id: true,
90
+ name: true,
91
+ email: true,
92
+ _count: { select: { posts: true } },
93
+ },
94
+ orderBy: { createdAt: 'desc' },
95
+ });
96
+
97
+ return (
98
+ <ul>
99
+ {users.map(user => (
100
+ <li key={user.id}>
101
+ {user.name} - {user._count.posts} posts
102
+ </li>
103
+ ))}
104
+ </ul>
105
+ );
106
+ }
107
+ ```
108
+
109
+ ### With Pagination
110
+
111
+ ```typescript
112
+ // app/posts/page.tsx
113
+ import { prisma } from '@/lib/prisma';
114
+
115
+ type Props = {
116
+ searchParams: Promise<{ page?: string; limit?: string }>;
117
+ };
118
+
119
+ export default async function PostsPage({ searchParams }: Props) {
120
+ const { page = '1', limit = '10' } = await searchParams;
121
+ const pageNum = parseInt(page);
122
+ const limitNum = parseInt(limit);
123
+
124
+ const [posts, total] = await Promise.all([
125
+ prisma.post.findMany({
126
+ where: { published: true },
127
+ include: { author: { select: { name: true } } },
128
+ skip: (pageNum - 1) * limitNum,
129
+ take: limitNum,
130
+ orderBy: { createdAt: 'desc' },
131
+ }),
132
+ prisma.post.count({ where: { published: true } }),
133
+ ]);
134
+
135
+ const totalPages = Math.ceil(total / limitNum);
136
+
137
+ return (
138
+ <>
139
+ <PostList posts={posts} />
140
+ <Pagination
141
+ currentPage={pageNum}
142
+ totalPages={totalPages}
143
+ />
144
+ </>
145
+ );
146
+ }
147
+ ```
148
+
149
+ ### Server Actions
150
+
151
+ ```typescript
152
+ // app/posts/actions.ts
153
+ 'use server';
154
+
155
+ import { prisma } from '@/lib/prisma';
156
+ import { auth } from '@/auth';
157
+ import { revalidatePath } from 'next/cache';
158
+ import { z } from 'zod';
159
+
160
+ const createPostSchema = z.object({
161
+ title: z.string().min(1).max(200),
162
+ content: z.string().optional(),
163
+ });
164
+
165
+ export async function createPost(formData: FormData) {
166
+ const session = await auth();
167
+ if (!session) throw new Error('Unauthorized');
168
+
169
+ const data = createPostSchema.parse({
170
+ title: formData.get('title'),
171
+ content: formData.get('content'),
172
+ });
173
+
174
+ const slug = data.title
175
+ .toLowerCase()
176
+ .replace(/[^a-z0-9]+/g, '-')
177
+ .replace(/(^-|-$)/g, '');
178
+
179
+ const post = await prisma.post.create({
180
+ data: {
181
+ ...data,
182
+ slug,
183
+ authorId: session.user.id,
184
+ },
185
+ });
186
+
187
+ revalidatePath('/posts');
188
+ return post;
189
+ }
190
+
191
+ export async function deletePost(id: string) {
192
+ const session = await auth();
193
+ if (!session) throw new Error('Unauthorized');
194
+
195
+ const post = await prisma.post.findUnique({
196
+ where: { id },
197
+ select: { authorId: true },
198
+ });
199
+
200
+ if (post?.authorId !== session.user.id) {
201
+ throw new Error('Forbidden');
202
+ }
203
+
204
+ await prisma.post.delete({ where: { id } });
205
+ revalidatePath('/posts');
206
+ }
207
+ ```
208
+
209
+ ### Transactions
210
+
211
+ ```typescript
212
+ export async function transferCredits(
213
+ fromUserId: string,
214
+ toUserId: string,
215
+ amount: number
216
+ ) {
217
+ return prisma.$transaction(async (tx) => {
218
+ const sender = await tx.user.update({
219
+ where: { id: fromUserId },
220
+ data: { credits: { decrement: amount } },
221
+ });
222
+
223
+ if (sender.credits < 0) {
224
+ throw new Error('Insufficient credits');
225
+ }
226
+
227
+ const recipient = await tx.user.update({
228
+ where: { id: toUserId },
229
+ data: { credits: { increment: amount } },
230
+ });
231
+
232
+ await tx.transaction.create({
233
+ data: {
234
+ fromUserId,
235
+ toUserId,
236
+ amount,
237
+ type: 'TRANSFER',
238
+ },
239
+ });
240
+
241
+ return { sender, recipient };
242
+ });
243
+ }
244
+ ```
245
+
246
+ ## Migrations
247
+
248
+ ```bash
249
+ # Create migration
250
+ npx prisma migrate dev --name add_user_role
251
+
252
+ # Apply migrations (production)
253
+ npx prisma migrate deploy
254
+
255
+ # Reset database (development)
256
+ npx prisma migrate reset
257
+
258
+ # Generate client
259
+ npx prisma generate
260
+ ```
261
+
262
+ ## Seeding
263
+
264
+ ```typescript
265
+ // prisma/seed.ts
266
+ import { PrismaClient } from '@prisma/client';
267
+
268
+ const prisma = new PrismaClient();
269
+
270
+ async function main() {
271
+ // Clean up
272
+ await prisma.post.deleteMany();
273
+ await prisma.user.deleteMany();
274
+
275
+ // Create users
276
+ const alice = await prisma.user.create({
277
+ data: {
278
+ email: 'alice@example.com',
279
+ name: 'Alice',
280
+ role: 'ADMIN',
281
+ posts: {
282
+ create: [
283
+ { title: 'First Post', slug: 'first-post', published: true },
284
+ { title: 'Second Post', slug: 'second-post' },
285
+ ],
286
+ },
287
+ },
288
+ });
289
+
290
+ console.log({ alice });
291
+ }
292
+
293
+ main()
294
+ .catch(console.error)
295
+ .finally(() => prisma.$disconnect());
296
+ ```
297
+
298
+ ```json
299
+ // package.json
300
+ {
301
+ "prisma": {
302
+ "seed": "tsx prisma/seed.ts"
303
+ }
304
+ }
305
+ ```
306
+
307
+ ## Soft Deletes
308
+
309
+ ```prisma
310
+ model User {
311
+ id String @id @default(cuid())
312
+ email String @unique
313
+ deletedAt DateTime?
314
+
315
+ @@index([deletedAt])
316
+ }
317
+ ```
318
+
319
+ ```typescript
320
+ // Middleware for soft deletes
321
+ prisma.$use(async (params, next) => {
322
+ if (params.model === 'User') {
323
+ if (params.action === 'delete') {
324
+ params.action = 'update';
325
+ params.args['data'] = { deletedAt: new Date() };
326
+ }
327
+ if (params.action === 'findMany' || params.action === 'findFirst') {
328
+ params.args['where'] = {
329
+ ...params.args['where'],
330
+ deletedAt: null,
331
+ };
332
+ }
333
+ }
334
+ return next(params);
335
+ });
336
+ ```
337
+
338
+ ## Optimization
339
+
340
+ ### Select Only Needed Fields
341
+
342
+ ```typescript
343
+ // BAD: Fetches all fields
344
+ const users = await prisma.user.findMany();
345
+
346
+ // GOOD: Select only needed
347
+ const users = await prisma.user.findMany({
348
+ select: { id: true, name: true, email: true },
349
+ });
350
+ ```
351
+
352
+ ### Avoid N+1
353
+
354
+ ```typescript
355
+ // BAD: N+1 query
356
+ const posts = await prisma.post.findMany();
357
+ for (const post of posts) {
358
+ const author = await prisma.user.findUnique({ where: { id: post.authorId } });
359
+ }
360
+
361
+ // GOOD: Include relation
362
+ const posts = await prisma.post.findMany({
363
+ include: { author: { select: { name: true } } },
364
+ });
365
+ ```
366
+
367
+ ### Connection Pooling (Serverless)
368
+
369
+ ```typescript
370
+ // For serverless (Vercel, AWS Lambda)
371
+ datasource db {
372
+ provider = "postgresql"
373
+ url = env("DATABASE_URL")
374
+ directUrl = env("DIRECT_URL") // For migrations
375
+ }
376
+
377
+ // Use connection pooler (PgBouncer, Prisma Accelerate)
378
+ // DATABASE_URL="prisma://accelerate.prisma-data.net/?api_key=..."
379
+ ```
380
+
381
+ ## Anti-patterns
382
+
383
+ ```typescript
384
+ // BAD: Creating client in component
385
+ export default async function Page() {
386
+ const prisma = new PrismaClient(); // New connection each request!
387
+ }
388
+
389
+ // GOOD: Use singleton
390
+ import { prisma } from '@/lib/prisma';
391
+
392
+ // BAD: Not handling errors
393
+ const user = await prisma.user.findUnique({ where: { id } });
394
+ return user.name; // Might be null!
395
+
396
+ // GOOD: Handle null
397
+ const user = await prisma.user.findUnique({ where: { id } });
398
+ if (!user) notFound();
399
+ return user.name;
400
+ ```
@@ -0,0 +1,303 @@
1
+ ---
2
+ paths:
3
+ - "middleware.ts"
4
+ - "src/middleware.ts"
5
+ ---
6
+
7
+ # Next.js Middleware
8
+
9
+ ## Basic Structure
10
+
11
+ ```typescript
12
+ // middleware.ts
13
+ import { NextResponse } from 'next/server';
14
+ import type { NextRequest } from 'next/server';
15
+
16
+ export function middleware(request: NextRequest) {
17
+ // Runs on every matched request
18
+ return NextResponse.next();
19
+ }
20
+
21
+ export const config = {
22
+ matcher: [
23
+ // Match all paths except static files
24
+ '/((?!_next/static|_next/image|favicon.ico).*)',
25
+ ],
26
+ };
27
+ ```
28
+
29
+ ## Common Patterns
30
+
31
+ ### Authentication
32
+
33
+ ```typescript
34
+ import { NextResponse } from 'next/server';
35
+ import type { NextRequest } from 'next/server';
36
+ import { getToken } from 'next-auth/jwt';
37
+
38
+ export async function middleware(request: NextRequest) {
39
+ const token = await getToken({ req: request });
40
+ const { pathname } = request.nextUrl;
41
+
42
+ // Public paths
43
+ const publicPaths = ['/login', '/register', '/api/auth'];
44
+ if (publicPaths.some(path => pathname.startsWith(path))) {
45
+ return NextResponse.next();
46
+ }
47
+
48
+ // Protected paths
49
+ if (!token) {
50
+ const loginUrl = new URL('/login', request.url);
51
+ loginUrl.searchParams.set('callbackUrl', pathname);
52
+ return NextResponse.redirect(loginUrl);
53
+ }
54
+
55
+ return NextResponse.next();
56
+ }
57
+ ```
58
+
59
+ ### Role-Based Access
60
+
61
+ ```typescript
62
+ export async function middleware(request: NextRequest) {
63
+ const token = await getToken({ req: request });
64
+ const { pathname } = request.nextUrl;
65
+
66
+ // Admin routes
67
+ if (pathname.startsWith('/admin')) {
68
+ if (token?.role !== 'admin') {
69
+ return NextResponse.redirect(new URL('/forbidden', request.url));
70
+ }
71
+ }
72
+
73
+ // API admin routes
74
+ if (pathname.startsWith('/api/admin')) {
75
+ if (token?.role !== 'admin') {
76
+ return NextResponse.json(
77
+ { error: 'Forbidden' },
78
+ { status: 403 }
79
+ );
80
+ }
81
+ }
82
+
83
+ return NextResponse.next();
84
+ }
85
+ ```
86
+
87
+ ### Internationalization (i18n)
88
+
89
+ ```typescript
90
+ import { NextResponse } from 'next/server';
91
+ import type { NextRequest } from 'next/server';
92
+ import { match } from '@formatjs/intl-localematcher';
93
+ import Negotiator from 'negotiator';
94
+
95
+ const locales = ['en', 'fr', 'de'];
96
+ const defaultLocale = 'en';
97
+
98
+ function getLocale(request: NextRequest): string {
99
+ const headers = { 'accept-language': request.headers.get('accept-language') || '' };
100
+ const languages = new Negotiator({ headers }).languages();
101
+ return match(languages, locales, defaultLocale);
102
+ }
103
+
104
+ export function middleware(request: NextRequest) {
105
+ const { pathname } = request.nextUrl;
106
+
107
+ // Check if pathname has locale
108
+ const pathnameHasLocale = locales.some(
109
+ locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
110
+ );
111
+
112
+ if (pathnameHasLocale) return NextResponse.next();
113
+
114
+ // Redirect to locale
115
+ const locale = getLocale(request);
116
+ request.nextUrl.pathname = `/${locale}${pathname}`;
117
+ return NextResponse.redirect(request.nextUrl);
118
+ }
119
+
120
+ export const config = {
121
+ matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
122
+ };
123
+ ```
124
+
125
+ ### Rate Limiting
126
+
127
+ ```typescript
128
+ import { NextResponse } from 'next/server';
129
+ import type { NextRequest } from 'next/server';
130
+ import { Ratelimit } from '@upstash/ratelimit';
131
+ import { Redis } from '@upstash/redis';
132
+
133
+ const ratelimit = new Ratelimit({
134
+ redis: Redis.fromEnv(),
135
+ limiter: Ratelimit.slidingWindow(10, '10 s'),
136
+ });
137
+
138
+ export async function middleware(request: NextRequest) {
139
+ if (request.nextUrl.pathname.startsWith('/api')) {
140
+ const ip = request.ip ?? '127.0.0.1';
141
+ const { success, limit, remaining, reset } = await ratelimit.limit(ip);
142
+
143
+ if (!success) {
144
+ return NextResponse.json(
145
+ { error: 'Too many requests' },
146
+ {
147
+ status: 429,
148
+ headers: {
149
+ 'X-RateLimit-Limit': limit.toString(),
150
+ 'X-RateLimit-Remaining': remaining.toString(),
151
+ 'X-RateLimit-Reset': reset.toString(),
152
+ },
153
+ }
154
+ );
155
+ }
156
+ }
157
+
158
+ return NextResponse.next();
159
+ }
160
+ ```
161
+
162
+ ### Geolocation Redirect
163
+
164
+ ```typescript
165
+ export function middleware(request: NextRequest) {
166
+ const country = request.geo?.country || 'US';
167
+
168
+ // Redirect EU users to EU subdomain
169
+ const euCountries = ['DE', 'FR', 'IT', 'ES', 'NL'];
170
+ if (euCountries.includes(country) && !request.nextUrl.hostname.includes('eu.')) {
171
+ return NextResponse.redirect(
172
+ new URL(request.nextUrl.pathname, 'https://eu.example.com')
173
+ );
174
+ }
175
+
176
+ return NextResponse.next();
177
+ }
178
+ ```
179
+
180
+ ### Security Headers
181
+
182
+ ```typescript
183
+ export function middleware(request: NextRequest) {
184
+ const response = NextResponse.next();
185
+
186
+ // Security headers
187
+ response.headers.set('X-Frame-Options', 'DENY');
188
+ response.headers.set('X-Content-Type-Options', 'nosniff');
189
+ response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
190
+ response.headers.set(
191
+ 'Content-Security-Policy',
192
+ "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';"
193
+ );
194
+ response.headers.set(
195
+ 'Permissions-Policy',
196
+ 'camera=(), microphone=(), geolocation=()'
197
+ );
198
+
199
+ return response;
200
+ }
201
+ ```
202
+
203
+ ### Request/Response Modification
204
+
205
+ ```typescript
206
+ export function middleware(request: NextRequest) {
207
+ // Add custom header to request
208
+ const requestHeaders = new Headers(request.headers);
209
+ requestHeaders.set('x-request-id', crypto.randomUUID());
210
+
211
+ // Rewrite URL
212
+ if (request.nextUrl.pathname === '/old-path') {
213
+ return NextResponse.rewrite(new URL('/new-path', request.url));
214
+ }
215
+
216
+ // Pass headers to server components
217
+ const response = NextResponse.next({
218
+ request: {
219
+ headers: requestHeaders,
220
+ },
221
+ });
222
+
223
+ // Add header to response
224
+ response.headers.set('x-request-id', requestHeaders.get('x-request-id')!);
225
+
226
+ return response;
227
+ }
228
+ ```
229
+
230
+ ### A/B Testing
231
+
232
+ ```typescript
233
+ export function middleware(request: NextRequest) {
234
+ const bucket = request.cookies.get('ab-bucket')?.value;
235
+
236
+ if (!bucket) {
237
+ // Assign to bucket
238
+ const newBucket = Math.random() > 0.5 ? 'a' : 'b';
239
+ const response = NextResponse.next();
240
+ response.cookies.set('ab-bucket', newBucket, {
241
+ httpOnly: true,
242
+ maxAge: 60 * 60 * 24 * 30, // 30 days
243
+ });
244
+ return response;
245
+ }
246
+
247
+ // Rewrite based on bucket
248
+ if (request.nextUrl.pathname === '/landing') {
249
+ return NextResponse.rewrite(
250
+ new URL(`/landing-${bucket}`, request.url)
251
+ );
252
+ }
253
+
254
+ return NextResponse.next();
255
+ }
256
+ ```
257
+
258
+ ## Matcher Patterns
259
+
260
+ ```typescript
261
+ export const config = {
262
+ matcher: [
263
+ // Match all paths except static
264
+ '/((?!_next/static|_next/image|favicon.ico).*)',
265
+
266
+ // Match specific paths
267
+ '/dashboard/:path*',
268
+ '/api/:path*',
269
+
270
+ // Match with regex
271
+ '/(api|admin)/:path*',
272
+
273
+ // Exclude specific paths
274
+ '/((?!api/public)api/:path*)',
275
+ ],
276
+ };
277
+ ```
278
+
279
+ ## Anti-patterns
280
+
281
+ ```typescript
282
+ // BAD: Heavy computation in middleware (runs on every request)
283
+ export async function middleware(request: NextRequest) {
284
+ await heavyDatabaseQuery(); // Blocks all requests!
285
+ }
286
+
287
+ // GOOD: Keep middleware lightweight
288
+ export async function middleware(request: NextRequest) {
289
+ // Only quick checks, use Edge-compatible code
290
+ }
291
+
292
+ // BAD: Using Node.js APIs (middleware runs on Edge)
293
+ import fs from 'fs'; // Won't work!
294
+
295
+ // GOOD: Use Edge-compatible APIs
296
+ import { Redis } from '@upstash/redis'; // Edge-compatible
297
+
298
+ // BAD: Modifying response body
299
+ return new Response('Modified body'); // Loses Next.js features
300
+
301
+ // GOOD: Use rewrite or redirect
302
+ return NextResponse.rewrite(new URL('/new-path', request.url));
303
+ ```