@jaimevalasek/aioson 1.3.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.
- package/CHANGELOG.md +456 -0
- package/CODE_OF_CONDUCT.md +12 -0
- package/CONTRIBUTING.md +13 -0
- package/LICENSE +21 -0
- package/README.md +254 -0
- package/bin/aioson.js +4 -0
- package/docs/en/cli-reference.md +398 -0
- package/docs/en/i18n.md +52 -0
- package/docs/en/json-schemas.md +41 -0
- package/docs/en/mcp.md +56 -0
- package/docs/en/parallel.md +82 -0
- package/docs/en/qa-browser.md +339 -0
- package/docs/en/release-flow.md +22 -0
- package/docs/en/release-notes-template.md +41 -0
- package/docs/en/release.md +28 -0
- package/docs/en/schemas/agent-prompt.schema.json +17 -0
- package/docs/en/schemas/agents.schema.json +32 -0
- package/docs/en/schemas/context-validate.schema.json +36 -0
- package/docs/en/schemas/doctor.schema.json +89 -0
- package/docs/en/schemas/error.schema.json +24 -0
- package/docs/en/schemas/i18n-add.schema.json +15 -0
- package/docs/en/schemas/index.json +116 -0
- package/docs/en/schemas/info.schema.json +39 -0
- package/docs/en/schemas/init.schema.json +48 -0
- package/docs/en/schemas/install.schema.json +60 -0
- package/docs/en/schemas/locale-apply.schema.json +30 -0
- package/docs/en/schemas/mcp-doctor.schema.json +95 -0
- package/docs/en/schemas/mcp-init.schema.json +122 -0
- package/docs/en/schemas/package-test.schema.json +24 -0
- package/docs/en/schemas/parallel-assign.schema.json +57 -0
- package/docs/en/schemas/parallel-doctor.schema.json +86 -0
- package/docs/en/schemas/parallel-init.schema.json +53 -0
- package/docs/en/schemas/parallel-status.schema.json +94 -0
- package/docs/en/schemas/setup-context.schema.json +39 -0
- package/docs/en/schemas/smoke.schema.json +23 -0
- package/docs/en/schemas/update.schema.json +48 -0
- package/docs/en/schemas/workflow-plan.schema.json +30 -0
- package/docs/en/web3.md +54 -0
- package/docs/pt/README.md +46 -0
- package/docs/pt/advisor-spec.md +335 -0
- package/docs/pt/agentes.md +453 -0
- package/docs/pt/cenarios.md +1230 -0
- package/docs/pt/clientes-ai.md +224 -0
- package/docs/pt/comandos-cli.md +511 -0
- package/docs/pt/genome-3.0-spec.md +296 -0
- package/docs/pt/guia-engineer.md +226 -0
- package/docs/pt/inicio-rapido.md +138 -0
- package/docs/pt/profiler-system.md +214 -0
- package/docs/pt/runtime-observability.md +72 -0
- package/docs/pt/squad-genoma.md +777 -0
- package/docs/pt/web3.md +797 -0
- package/docs/testing/genome-2.0-manual-regression.md +23 -0
- package/docs/testing/genome-2.0-matrix.md +36 -0
- package/docs/testing/genome-2.0-rollout.md +184 -0
- package/package.json +50 -0
- package/src/agents.js +56 -0
- package/src/cli.js +497 -0
- package/src/commands/agents.js +142 -0
- package/src/commands/cloud.js +1767 -0
- package/src/commands/config.js +90 -0
- package/src/commands/context-validate.js +91 -0
- package/src/commands/doctor.js +123 -0
- package/src/commands/genome-doctor.js +41 -0
- package/src/commands/genome-migrate.js +49 -0
- package/src/commands/i18n-add.js +56 -0
- package/src/commands/info.js +41 -0
- package/src/commands/init.js +75 -0
- package/src/commands/install.js +68 -0
- package/src/commands/locale-apply.js +51 -0
- package/src/commands/locale-diff.js +126 -0
- package/src/commands/mcp-doctor.js +406 -0
- package/src/commands/mcp-init.js +379 -0
- package/src/commands/package-e2e.js +273 -0
- package/src/commands/parallel-assign.js +403 -0
- package/src/commands/parallel-doctor.js +437 -0
- package/src/commands/parallel-init.js +249 -0
- package/src/commands/parallel-status.js +290 -0
- package/src/commands/qa-doctor.js +185 -0
- package/src/commands/qa-init.js +161 -0
- package/src/commands/qa-report.js +58 -0
- package/src/commands/qa-run.js +873 -0
- package/src/commands/qa-scan.js +337 -0
- package/src/commands/runtime.js +948 -0
- package/src/commands/scan-project.js +1107 -0
- package/src/commands/setup-context.js +650 -0
- package/src/commands/smoke.js +426 -0
- package/src/commands/squad-doctor.js +358 -0
- package/src/commands/squad-export.js +46 -0
- package/src/commands/squad-pipeline.js +97 -0
- package/src/commands/squad-repair-genomes.js +39 -0
- package/src/commands/squad-status.js +424 -0
- package/src/commands/squad-validate.js +230 -0
- package/src/commands/test-agents.js +194 -0
- package/src/commands/update.js +55 -0
- package/src/commands/workflow-next.js +594 -0
- package/src/commands/workflow-plan.js +108 -0
- package/src/constants.js +314 -0
- package/src/context-parse-reason.js +22 -0
- package/src/context-writer.js +150 -0
- package/src/context.js +217 -0
- package/src/detector.js +261 -0
- package/src/doctor.js +289 -0
- package/src/execution-gateway.js +461 -0
- package/src/genome-files.js +198 -0
- package/src/genome-format.js +442 -0
- package/src/genome-schema.js +215 -0
- package/src/genomes/bindings.js +281 -0
- package/src/genomes.js +467 -0
- package/src/i18n/index.js +103 -0
- package/src/i18n/messages/en.js +784 -0
- package/src/i18n/messages/es.js +718 -0
- package/src/i18n/messages/fr.js +725 -0
- package/src/i18n/messages/pt-BR.js +818 -0
- package/src/i18n/scaffold.js +64 -0
- package/src/installer.js +232 -0
- package/src/lib/genomes/compat.js +206 -0
- package/src/lib/genomes/migrate.js +90 -0
- package/src/lib/squads/genome-repair.js +49 -0
- package/src/locales.js +84 -0
- package/src/onboarding.js +305 -0
- package/src/parser.js +53 -0
- package/src/prompt-tool.js +20 -0
- package/src/qa-html-report.js +472 -0
- package/src/runtime-store.js +1527 -0
- package/src/squads/apply-genome.js +21 -0
- package/src/squads/genome-binding-service.js +154 -0
- package/src/updater.js +32 -0
- package/src/utils.js +46 -0
- package/src/version.js +50 -0
- package/template/.aioson/advisors/.gitkeep +1 -0
- package/template/.aioson/agents/analyst.md +225 -0
- package/template/.aioson/agents/architect.md +221 -0
- package/template/.aioson/agents/dev.md +201 -0
- package/template/.aioson/agents/discovery-design-doc.md +196 -0
- package/template/.aioson/agents/genoma.md +300 -0
- package/template/.aioson/agents/orchestrator.md +107 -0
- package/template/.aioson/agents/pm.md +89 -0
- package/template/.aioson/agents/product.md +361 -0
- package/template/.aioson/agents/profiler-enricher.md +266 -0
- package/template/.aioson/agents/profiler-forge.md +188 -0
- package/template/.aioson/agents/profiler-researcher.md +245 -0
- package/template/.aioson/agents/qa.md +344 -0
- package/template/.aioson/agents/setup.md +381 -0
- package/template/.aioson/agents/squad.md +837 -0
- package/template/.aioson/agents/ux-ui.md +416 -0
- package/template/.aioson/config.md +56 -0
- package/template/.aioson/context/.gitkeep +0 -0
- package/template/.aioson/context/parallel/.gitkeep +0 -0
- package/template/.aioson/context/spec.md.template +37 -0
- package/template/.aioson/genomas/.gitkeep +0 -0
- package/template/.aioson/locales/en/agents/analyst.md +214 -0
- package/template/.aioson/locales/en/agents/architect.md +210 -0
- package/template/.aioson/locales/en/agents/dev.md +187 -0
- package/template/.aioson/locales/en/agents/discovery-design-doc.md +27 -0
- package/template/.aioson/locales/en/agents/genoma.md +212 -0
- package/template/.aioson/locales/en/agents/orchestrator.md +105 -0
- package/template/.aioson/locales/en/agents/pm.md +77 -0
- package/template/.aioson/locales/en/agents/product.md +310 -0
- package/template/.aioson/locales/en/agents/profiler-enricher.md +5 -0
- package/template/.aioson/locales/en/agents/profiler-forge.md +5 -0
- package/template/.aioson/locales/en/agents/profiler-researcher.md +5 -0
- package/template/.aioson/locales/en/agents/qa.md +214 -0
- package/template/.aioson/locales/en/agents/setup.md +342 -0
- package/template/.aioson/locales/en/agents/squad.md +247 -0
- package/template/.aioson/locales/en/agents/ux-ui.md +320 -0
- package/template/.aioson/locales/es/agents/analyst.md +203 -0
- package/template/.aioson/locales/es/agents/architect.md +208 -0
- package/template/.aioson/locales/es/agents/dev.md +183 -0
- package/template/.aioson/locales/es/agents/discovery-design-doc.md +19 -0
- package/template/.aioson/locales/es/agents/genoma.md +102 -0
- package/template/.aioson/locales/es/agents/orchestrator.md +108 -0
- package/template/.aioson/locales/es/agents/pm.md +81 -0
- package/template/.aioson/locales/es/agents/product.md +310 -0
- package/template/.aioson/locales/es/agents/profiler-enricher.md +5 -0
- package/template/.aioson/locales/es/agents/profiler-forge.md +5 -0
- package/template/.aioson/locales/es/agents/profiler-researcher.md +5 -0
- package/template/.aioson/locales/es/agents/qa.md +163 -0
- package/template/.aioson/locales/es/agents/setup.md +347 -0
- package/template/.aioson/locales/es/agents/squad.md +247 -0
- package/template/.aioson/locales/es/agents/ux-ui.md +201 -0
- package/template/.aioson/locales/fr/agents/analyst.md +203 -0
- package/template/.aioson/locales/fr/agents/architect.md +208 -0
- package/template/.aioson/locales/fr/agents/dev.md +183 -0
- package/template/.aioson/locales/fr/agents/discovery-design-doc.md +19 -0
- package/template/.aioson/locales/fr/agents/genoma.md +102 -0
- package/template/.aioson/locales/fr/agents/orchestrator.md +108 -0
- package/template/.aioson/locales/fr/agents/pm.md +81 -0
- package/template/.aioson/locales/fr/agents/product.md +310 -0
- package/template/.aioson/locales/fr/agents/profiler-enricher.md +5 -0
- package/template/.aioson/locales/fr/agents/profiler-forge.md +5 -0
- package/template/.aioson/locales/fr/agents/profiler-researcher.md +5 -0
- package/template/.aioson/locales/fr/agents/qa.md +163 -0
- package/template/.aioson/locales/fr/agents/setup.md +347 -0
- package/template/.aioson/locales/fr/agents/squad.md +247 -0
- package/template/.aioson/locales/fr/agents/ux-ui.md +201 -0
- package/template/.aioson/locales/pt-BR/agents/analyst.md +217 -0
- package/template/.aioson/locales/pt-BR/agents/architect.md +213 -0
- package/template/.aioson/locales/pt-BR/agents/dev.md +198 -0
- package/template/.aioson/locales/pt-BR/agents/discovery-design-doc.md +198 -0
- package/template/.aioson/locales/pt-BR/agents/genoma.md +297 -0
- package/template/.aioson/locales/pt-BR/agents/orchestrator.md +108 -0
- package/template/.aioson/locales/pt-BR/agents/pm.md +81 -0
- package/template/.aioson/locales/pt-BR/agents/product.md +316 -0
- package/template/.aioson/locales/pt-BR/agents/profiler-enricher.md +5 -0
- package/template/.aioson/locales/pt-BR/agents/profiler-forge.md +5 -0
- package/template/.aioson/locales/pt-BR/agents/profiler-researcher.md +5 -0
- package/template/.aioson/locales/pt-BR/agents/qa.md +217 -0
- package/template/.aioson/locales/pt-BR/agents/setup.md +371 -0
- package/template/.aioson/locales/pt-BR/agents/squad.md +772 -0
- package/template/.aioson/locales/pt-BR/agents/ux-ui.md +322 -0
- package/template/.aioson/mcp/servers.md +24 -0
- package/template/.aioson/profiler-reports/.gitkeep +1 -0
- package/template/.aioson/schemas/content-blueprint.schema.json +30 -0
- package/template/.aioson/schemas/genome-meta.schema.json +150 -0
- package/template/.aioson/schemas/genome.schema.json +115 -0
- package/template/.aioson/schemas/readiness.schema.json +27 -0
- package/template/.aioson/schemas/squad-blueprint.schema.json +172 -0
- package/template/.aioson/schemas/squad-manifest.schema.json +276 -0
- package/template/.aioson/skills/dynamic/README.md +30 -0
- package/template/.aioson/skills/dynamic/cardano-docs.md +16 -0
- package/template/.aioson/skills/dynamic/ethereum-docs.md +17 -0
- package/template/.aioson/skills/dynamic/flux-ui-docs.md +13 -0
- package/template/.aioson/skills/dynamic/laravel-docs.md +41 -0
- package/template/.aioson/skills/dynamic/npm-packages.md +16 -0
- package/template/.aioson/skills/dynamic/solana-docs.md +16 -0
- package/template/.aioson/skills/references/premium-command-center-ui/master-application-prompt.md +79 -0
- package/template/.aioson/skills/references/premium-command-center-ui/operational-ux-playbook.md +253 -0
- package/template/.aioson/skills/references/premium-command-center-ui/quality-validation-checklist.md +82 -0
- package/template/.aioson/skills/references/premium-command-center-ui/visual-system-and-component-patterns.md +270 -0
- package/template/.aioson/skills/static/django-patterns.md +342 -0
- package/template/.aioson/skills/static/fastapi-patterns.md +344 -0
- package/template/.aioson/skills/static/filament-patterns.md +267 -0
- package/template/.aioson/skills/static/flux-ui-components.md +262 -0
- package/template/.aioson/skills/static/git-conventions.md +227 -0
- package/template/.aioson/skills/static/interface-design.md +372 -0
- package/template/.aioson/skills/static/jetstream-setup.md +200 -0
- package/template/.aioson/skills/static/laravel-conventions.md +491 -0
- package/template/.aioson/skills/static/nextjs-patterns.md +321 -0
- package/template/.aioson/skills/static/node-express-patterns.md +317 -0
- package/template/.aioson/skills/static/node-typescript-patterns.md +282 -0
- package/template/.aioson/skills/static/premium-command-center-ui.md +190 -0
- package/template/.aioson/skills/static/rails-conventions.md +307 -0
- package/template/.aioson/skills/static/react-motion-patterns.md +577 -0
- package/template/.aioson/skills/static/static-html-patterns.md +1935 -0
- package/template/.aioson/skills/static/tall-stack-patterns.md +286 -0
- package/template/.aioson/skills/static/ui-ux-modern.md +75 -0
- package/template/.aioson/skills/static/web3-cardano-patterns.md +337 -0
- package/template/.aioson/skills/static/web3-ethereum-patterns.md +310 -0
- package/template/.aioson/skills/static/web3-security-checklist.md +284 -0
- package/template/.aioson/skills/static/web3-solana-patterns.md +324 -0
- package/template/.aioson/squads/.artisan/.gitkeep +0 -0
- package/template/.aioson/squads/.gitkeep +0 -0
- package/template/.aioson/squads/memory.md +5 -0
- package/template/.aioson/tasks/squad-analyze.md +83 -0
- package/template/.aioson/tasks/squad-create.md +99 -0
- package/template/.aioson/tasks/squad-design.md +100 -0
- package/template/.aioson/tasks/squad-export.md +20 -0
- package/template/.aioson/tasks/squad-extend.md +68 -0
- package/template/.aioson/tasks/squad-pipeline.md +122 -0
- package/template/.aioson/tasks/squad-repair.md +85 -0
- package/template/.aioson/tasks/squad-validate.md +58 -0
- package/template/.aioson/templates/squads/content-basic/template.json +21 -0
- package/template/.aioson/templates/squads/media-channel/template.json +24 -0
- package/template/.aioson/templates/squads/research-analysis/template.json +22 -0
- package/template/.aioson/templates/squads/software-delivery/template.json +21 -0
- package/template/.claude/commands/aioson/analyst.md +5 -0
- package/template/.claude/commands/aioson/architect.md +5 -0
- package/template/.claude/commands/aioson/dev.md +5 -0
- package/template/.claude/commands/aioson/orchestrator.md +5 -0
- package/template/.claude/commands/aioson/pm.md +5 -0
- package/template/.claude/commands/aioson/qa.md +5 -0
- package/template/.claude/commands/aioson/setup.md +5 -0
- package/template/.claude/commands/aioson/ux-ui.md +5 -0
- package/template/.gemini/GEMINI.md +10 -0
- package/template/.gemini/commands/aios-analyst.toml +4 -0
- package/template/.gemini/commands/aios-architect.toml +7 -0
- package/template/.gemini/commands/aios-dev.toml +8 -0
- package/template/.gemini/commands/aios-discovery-design-doc.toml +4 -0
- package/template/.gemini/commands/aios-orchestrator.toml +8 -0
- package/template/.gemini/commands/aios-pm.toml +8 -0
- package/template/.gemini/commands/aios-product.toml +4 -0
- package/template/.gemini/commands/aios-qa.toml +6 -0
- package/template/.gemini/commands/aios-setup.toml +3 -0
- package/template/.gemini/commands/aios-ux-ui.toml +8 -0
- package/template/AGENTS.md +67 -0
- package/template/CLAUDE.md +31 -0
- package/template/OPENCODE.md +24 -0
- package/template/aioson-models.json +40 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# Next.js Patterns
|
|
2
|
+
|
|
3
|
+
> App Router, Server Components, and Server Actions. Build fast by default.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Mental model: Server vs Client
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Server Components (default) → fetch data, render HTML, never ship JS to browser
|
|
11
|
+
Client Components → interactivity, useState, useEffect, browser APIs
|
|
12
|
+
Server Actions → mutations from forms and client events, run on server
|
|
13
|
+
Route Handlers → REST-like API endpoints (for external consumers)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
**Rule:** Start every component as a Server Component. Add `"use client"` only when you need browser APIs, state, or event handlers.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Project structure (App Router)
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
src/
|
|
24
|
+
app/
|
|
25
|
+
(auth)/ ← route group, no URL segment
|
|
26
|
+
login/page.tsx
|
|
27
|
+
register/page.tsx
|
|
28
|
+
layout.tsx ← minimal layout for auth pages
|
|
29
|
+
(dashboard)/
|
|
30
|
+
layout.tsx ← authenticated shell (sidebar, nav)
|
|
31
|
+
page.tsx ← /dashboard
|
|
32
|
+
appointments/
|
|
33
|
+
page.tsx ← /appointments (list)
|
|
34
|
+
[id]/
|
|
35
|
+
page.tsx ← /appointments/123
|
|
36
|
+
edit/page.tsx ← /appointments/123/edit
|
|
37
|
+
settings/page.tsx
|
|
38
|
+
api/
|
|
39
|
+
webhooks/
|
|
40
|
+
stripe/route.ts ← POST /api/webhooks/stripe
|
|
41
|
+
components/
|
|
42
|
+
ui/ ← design system primitives (Button, Input, Modal)
|
|
43
|
+
features/
|
|
44
|
+
appointments/ ← feature-specific components
|
|
45
|
+
AppointmentCard.tsx
|
|
46
|
+
AppointmentList.tsx ← Server Component (fetches data)
|
|
47
|
+
BookingForm.tsx ← Client Component (form state)
|
|
48
|
+
lib/
|
|
49
|
+
db.ts ← Prisma client singleton
|
|
50
|
+
auth.ts ← NextAuth config
|
|
51
|
+
stripe.ts ← Stripe client
|
|
52
|
+
actions/ ← Server Actions
|
|
53
|
+
appointment.actions.ts
|
|
54
|
+
billing.actions.ts
|
|
55
|
+
types/
|
|
56
|
+
index.ts
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Data fetching — Server Components
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
// app/(dashboard)/appointments/page.tsx
|
|
65
|
+
// No useEffect, no useState, no API call — just async/await
|
|
66
|
+
export default async function AppointmentsPage() {
|
|
67
|
+
const session = await auth();
|
|
68
|
+
if (!session) redirect('/login');
|
|
69
|
+
|
|
70
|
+
const appointments = await db.appointment.findMany({
|
|
71
|
+
where: { userId: session.user.id },
|
|
72
|
+
include: { doctor: true },
|
|
73
|
+
orderBy: { date: 'asc' },
|
|
74
|
+
take: 50,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div>
|
|
79
|
+
<AppointmentList appointments={appointments} />
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// With suspense for streaming
|
|
85
|
+
export default function Page() {
|
|
86
|
+
return (
|
|
87
|
+
<Suspense fallback={<AppointmentListSkeleton />}>
|
|
88
|
+
<AppointmentList />
|
|
89
|
+
</Suspense>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Server Actions — mutations
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
// actions/appointment.actions.ts
|
|
100
|
+
'use server';
|
|
101
|
+
|
|
102
|
+
import { auth } from '@/lib/auth';
|
|
103
|
+
import { redirect } from 'next/navigation';
|
|
104
|
+
import { revalidatePath } from 'next/cache';
|
|
105
|
+
import { z } from 'zod';
|
|
106
|
+
|
|
107
|
+
const createSchema = z.object({
|
|
108
|
+
doctorId: z.string().uuid(),
|
|
109
|
+
date: z.string().datetime(),
|
|
110
|
+
notes: z.string().max(500).optional(),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
export async function createAppointment(formData: FormData) {
|
|
114
|
+
const session = await auth();
|
|
115
|
+
if (!session) throw new Error('Unauthorized');
|
|
116
|
+
|
|
117
|
+
const parsed = createSchema.safeParse({
|
|
118
|
+
doctorId: formData.get('doctorId'),
|
|
119
|
+
date: formData.get('date'),
|
|
120
|
+
notes: formData.get('notes'),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (!parsed.success) {
|
|
124
|
+
return { error: parsed.error.flatten().fieldErrors };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Check for conflicts
|
|
128
|
+
const conflict = await db.appointment.findFirst({
|
|
129
|
+
where: {
|
|
130
|
+
doctorId: parsed.data.doctorId,
|
|
131
|
+
date: new Date(parsed.data.date),
|
|
132
|
+
status: { not: 'cancelled' },
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (conflict) {
|
|
137
|
+
return { error: { date: ['This time slot is not available.'] } };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
await db.appointment.create({
|
|
141
|
+
data: { ...parsed.data, userId: session.user.id },
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
revalidatePath('/appointments');
|
|
145
|
+
redirect('/appointments');
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Usage in a Client Component:
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
'use client';
|
|
153
|
+
|
|
154
|
+
import { createAppointment } from '@/actions/appointment.actions';
|
|
155
|
+
import { useActionState } from 'react';
|
|
156
|
+
|
|
157
|
+
export function BookingForm({ doctors }: { doctors: Doctor[] }) {
|
|
158
|
+
const [state, action, pending] = useActionState(createAppointment, null);
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<form action={action}>
|
|
162
|
+
<select name="doctorId" required>
|
|
163
|
+
{doctors.map(d => (
|
|
164
|
+
<option key={d.id} value={d.id}>{d.name}</option>
|
|
165
|
+
))}
|
|
166
|
+
</select>
|
|
167
|
+
{state?.error?.doctorId && <p className="text-red-500">{state.error.doctorId[0]}</p>}
|
|
168
|
+
|
|
169
|
+
<input name="date" type="datetime-local" required />
|
|
170
|
+
{state?.error?.date && <p className="text-red-500">{state.error.date[0]}</p>}
|
|
171
|
+
|
|
172
|
+
<button type="submit" disabled={pending}>
|
|
173
|
+
{pending ? 'Booking...' : 'Book Appointment'}
|
|
174
|
+
</button>
|
|
175
|
+
</form>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Client Components — when and how
|
|
183
|
+
|
|
184
|
+
```tsx
|
|
185
|
+
'use client'; // Only add when needed
|
|
186
|
+
|
|
187
|
+
// Good reasons for "use client":
|
|
188
|
+
// - useState, useEffect, useReducer
|
|
189
|
+
// - Event handlers (onClick, onChange)
|
|
190
|
+
// - Browser APIs (localStorage, window, navigator)
|
|
191
|
+
// - Third-party libs that need DOM access
|
|
192
|
+
|
|
193
|
+
import { useState } from 'react';
|
|
194
|
+
|
|
195
|
+
export function ConfirmDialog({ onConfirm }: { onConfirm: () => void }) {
|
|
196
|
+
const [open, setOpen] = useState(false);
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<>
|
|
200
|
+
<button onClick={() => setOpen(true)}>Delete</button>
|
|
201
|
+
{open && (
|
|
202
|
+
<dialog open>
|
|
203
|
+
<p>Are you sure?</p>
|
|
204
|
+
<button onClick={() => { onConfirm(); setOpen(false); }}>Confirm</button>
|
|
205
|
+
<button onClick={() => setOpen(false)}>Cancel</button>
|
|
206
|
+
</dialog>
|
|
207
|
+
)}
|
|
208
|
+
</>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**Pass Server data as props — do not re-fetch in Client Components:**
|
|
214
|
+
|
|
215
|
+
```tsx
|
|
216
|
+
// WRONG — Client Component fetching its own data
|
|
217
|
+
'use client';
|
|
218
|
+
export function AppointmentList() {
|
|
219
|
+
const [appointments, setAppointments] = useState([]);
|
|
220
|
+
useEffect(() => { fetch('/api/appointments').then(...) }, []);
|
|
221
|
+
// ...
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// RIGHT — Server Component passes data as props
|
|
225
|
+
// app/page.tsx (Server)
|
|
226
|
+
const appointments = await db.appointment.findMany(...);
|
|
227
|
+
return <AppointmentList appointments={appointments} />;
|
|
228
|
+
|
|
229
|
+
// components/AppointmentList.tsx (can be Server too, or Client if interactive)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Route Handlers — for external consumers only
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
// app/api/webhooks/stripe/route.ts
|
|
238
|
+
import Stripe from 'stripe';
|
|
239
|
+
|
|
240
|
+
export async function POST(request: Request) {
|
|
241
|
+
const signature = request.headers.get('stripe-signature')!;
|
|
242
|
+
const body = await request.text();
|
|
243
|
+
|
|
244
|
+
let event: Stripe.Event;
|
|
245
|
+
try {
|
|
246
|
+
event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!);
|
|
247
|
+
} catch {
|
|
248
|
+
return new Response('Invalid signature', { status: 400 });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
switch (event.type) {
|
|
252
|
+
case 'invoice.paid':
|
|
253
|
+
await handleInvoicePaid(event.data.object as Stripe.Invoice);
|
|
254
|
+
break;
|
|
255
|
+
case 'customer.subscription.deleted':
|
|
256
|
+
await handleSubscriptionCancelled(event.data.object as Stripe.Subscription);
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return new Response('OK');
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Use Route Handlers for webhooks and external API consumers. Use Server Actions for mutations from your own UI.
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Metadata and SEO
|
|
269
|
+
|
|
270
|
+
```tsx
|
|
271
|
+
// app/appointments/[id]/page.tsx
|
|
272
|
+
import type { Metadata } from 'next';
|
|
273
|
+
|
|
274
|
+
export async function generateMetadata({ params }: { params: { id: string } }): Promise<Metadata> {
|
|
275
|
+
const appointment = await db.appointment.findUnique({ where: { id: params.id } });
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
title: `Appointment with ${appointment?.doctor.name}`,
|
|
279
|
+
description: `Scheduled for ${appointment?.date.toDateString()}`,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## Loading and error states
|
|
287
|
+
|
|
288
|
+
```tsx
|
|
289
|
+
// app/appointments/loading.tsx — shown while page.tsx is loading
|
|
290
|
+
export default function Loading() {
|
|
291
|
+
return <AppointmentListSkeleton />;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// app/appointments/error.tsx — caught by nearest error boundary
|
|
295
|
+
'use client';
|
|
296
|
+
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
|
297
|
+
return (
|
|
298
|
+
<div>
|
|
299
|
+
<p>Something went wrong: {error.message}</p>
|
|
300
|
+
<button onClick={reset}>Try again</button>
|
|
301
|
+
</div>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## ALWAYS
|
|
309
|
+
- Server Components by default — add `"use client"` only when needed
|
|
310
|
+
- Server Actions for all mutations from your UI
|
|
311
|
+
- Validate in Server Actions with Zod before touching the database
|
|
312
|
+
- `revalidatePath()` after mutations to refresh stale data
|
|
313
|
+
- `Suspense` boundaries with skeletons for streaming
|
|
314
|
+
- `generateMetadata()` for dynamic page titles
|
|
315
|
+
|
|
316
|
+
## NEVER
|
|
317
|
+
- `useEffect` to fetch data that could be fetched in a Server Component
|
|
318
|
+
- Route Handlers for mutations from your own frontend (use Server Actions)
|
|
319
|
+
- Client Components that re-fetch data they could receive as props
|
|
320
|
+
- `any` in TypeScript — define types for all Prisma responses and API payloads
|
|
321
|
+
- `process.env.*` in Client Components — use only in Server Components or Actions
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
# Node + Express Patterns
|
|
2
|
+
|
|
3
|
+
> Production-ready Express APIs. Clean layers, typed contracts, centralized error handling.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Project structure
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
src/
|
|
11
|
+
routes/ ← HTTP routing only
|
|
12
|
+
appointments.routes.ts
|
|
13
|
+
auth.routes.ts
|
|
14
|
+
index.ts ← registers all routers
|
|
15
|
+
controllers/ ← request parsing, response formatting
|
|
16
|
+
appointments.controller.ts
|
|
17
|
+
services/ ← business logic, domain operations
|
|
18
|
+
appointments.service.ts
|
|
19
|
+
email.service.ts
|
|
20
|
+
repositories/ ← data access layer (database queries)
|
|
21
|
+
appointments.repository.ts
|
|
22
|
+
middleware/
|
|
23
|
+
auth.middleware.ts
|
|
24
|
+
validate.middleware.ts
|
|
25
|
+
error.middleware.ts
|
|
26
|
+
rate-limit.middleware.ts
|
|
27
|
+
schemas/ ← Zod schemas for request validation
|
|
28
|
+
appointment.schema.ts
|
|
29
|
+
types/
|
|
30
|
+
index.ts
|
|
31
|
+
lib/
|
|
32
|
+
db.ts ← database client singleton
|
|
33
|
+
logger.ts ← winston/pino logger
|
|
34
|
+
app.ts ← Express app setup (no listen)
|
|
35
|
+
server.ts ← server startup + graceful shutdown
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Layer responsibilities
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
// routes — HTTP wiring only
|
|
44
|
+
// src/routes/appointments.routes.ts
|
|
45
|
+
const router = Router();
|
|
46
|
+
router.get('/', authenticate, AppointmentController.list);
|
|
47
|
+
router.post('/', authenticate, validate(createAppointmentSchema), AppointmentController.create);
|
|
48
|
+
router.delete('/:id', authenticate, AppointmentController.cancel);
|
|
49
|
+
export default router;
|
|
50
|
+
|
|
51
|
+
// controllers — parse request, call service, return response
|
|
52
|
+
// src/controllers/appointments.controller.ts
|
|
53
|
+
export const AppointmentController = {
|
|
54
|
+
create: async (req: Request, res: Response, next: NextFunction) => {
|
|
55
|
+
try {
|
|
56
|
+
const appointment = await AppointmentService.create(req.user!.id, req.body);
|
|
57
|
+
res.status(201).json({ data: appointment });
|
|
58
|
+
} catch (err) {
|
|
59
|
+
next(err);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
list: async (req: Request, res: Response, next: NextFunction) => {
|
|
64
|
+
try {
|
|
65
|
+
const { page = 1, limit = 20 } = req.query;
|
|
66
|
+
const result = await AppointmentService.listForUser(req.user!.id, { page: +page, limit: +limit });
|
|
67
|
+
res.json(result);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
next(err);
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// services — business logic
|
|
75
|
+
// src/services/appointments.service.ts
|
|
76
|
+
export const AppointmentService = {
|
|
77
|
+
create: async (userId: string, data: CreateAppointmentDto): Promise<Appointment> => {
|
|
78
|
+
const conflict = await AppointmentRepository.findConflict(data.doctorId, data.date);
|
|
79
|
+
if (conflict) throw new ConflictError('This time slot is already booked.');
|
|
80
|
+
|
|
81
|
+
const appointment = await AppointmentRepository.create({ ...data, userId });
|
|
82
|
+
await EmailService.sendConfirmation(appointment);
|
|
83
|
+
return appointment;
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// repositories — data access
|
|
88
|
+
// src/repositories/appointments.repository.ts
|
|
89
|
+
export const AppointmentRepository = {
|
|
90
|
+
findConflict: async (doctorId: string, date: Date): Promise<Appointment | null> => {
|
|
91
|
+
return db.appointment.findFirst({
|
|
92
|
+
where: { doctorId, date, status: { not: 'cancelled' } },
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
create: async (data: Prisma.AppointmentCreateInput): Promise<Appointment> => {
|
|
97
|
+
return db.appointment.create({ data, include: { doctor: true } });
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Validation middleware with Zod
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
// src/schemas/appointment.schema.ts
|
|
108
|
+
import { z } from 'zod';
|
|
109
|
+
|
|
110
|
+
export const createAppointmentSchema = z.object({
|
|
111
|
+
body: z.object({
|
|
112
|
+
doctorId: z.string().uuid('Invalid doctor ID'),
|
|
113
|
+
date: z.string().datetime('Invalid date format'),
|
|
114
|
+
notes: z.string().max(500).optional(),
|
|
115
|
+
}),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
export type CreateAppointmentDto = z.infer<typeof createAppointmentSchema>['body'];
|
|
119
|
+
|
|
120
|
+
// src/middleware/validate.middleware.ts
|
|
121
|
+
import { AnyZodObject, ZodError } from 'zod';
|
|
122
|
+
|
|
123
|
+
export const validate = (schema: AnyZodObject) =>
|
|
124
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
125
|
+
try {
|
|
126
|
+
await schema.parseAsync({
|
|
127
|
+
body: req.body,
|
|
128
|
+
query: req.query,
|
|
129
|
+
params: req.params,
|
|
130
|
+
});
|
|
131
|
+
next();
|
|
132
|
+
} catch (err) {
|
|
133
|
+
if (err instanceof ZodError) {
|
|
134
|
+
return res.status(422).json({
|
|
135
|
+
error: 'Validation failed',
|
|
136
|
+
details: err.flatten().fieldErrors,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
next(err);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Authentication middleware
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
// src/middleware/auth.middleware.ts
|
|
150
|
+
import jwt from 'jsonwebtoken';
|
|
151
|
+
|
|
152
|
+
export const authenticate = (req: Request, res: Response, next: NextFunction) => {
|
|
153
|
+
const token = req.headers.authorization?.split(' ')[1];
|
|
154
|
+
if (!token) return res.status(401).json({ error: 'Authentication required.' });
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const payload = jwt.verify(token, process.env.JWT_SECRET!) as JwtPayload;
|
|
158
|
+
req.user = { id: payload.sub!, role: payload.role };
|
|
159
|
+
next();
|
|
160
|
+
} catch {
|
|
161
|
+
res.status(401).json({ error: 'Invalid or expired token.' });
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
export const requireRole = (...roles: string[]) =>
|
|
166
|
+
(req: Request, res: Response, next: NextFunction) => {
|
|
167
|
+
if (!roles.includes(req.user!.role)) {
|
|
168
|
+
return res.status(403).json({ error: 'Insufficient permissions.' });
|
|
169
|
+
}
|
|
170
|
+
next();
|
|
171
|
+
};
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Centralized error handling
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
// src/lib/errors.ts — domain error classes
|
|
180
|
+
export class AppError extends Error {
|
|
181
|
+
constructor(public message: string, public statusCode: number) {
|
|
182
|
+
super(message);
|
|
183
|
+
this.name = this.constructor.name;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
export class NotFoundError extends AppError {
|
|
187
|
+
constructor(msg = 'Resource not found') { super(msg, 404); }
|
|
188
|
+
}
|
|
189
|
+
export class ConflictError extends AppError {
|
|
190
|
+
constructor(msg = 'Resource conflict') { super(msg, 409); }
|
|
191
|
+
}
|
|
192
|
+
export class ForbiddenError extends AppError {
|
|
193
|
+
constructor(msg = 'Forbidden') { super(msg, 403); }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/middleware/error.middleware.ts — MUST be last middleware registered
|
|
197
|
+
export const errorHandler = (
|
|
198
|
+
err: Error,
|
|
199
|
+
req: Request,
|
|
200
|
+
res: Response,
|
|
201
|
+
next: NextFunction
|
|
202
|
+
) => {
|
|
203
|
+
if (err instanceof AppError) {
|
|
204
|
+
return res.status(err.statusCode).json({ error: err.message });
|
|
205
|
+
}
|
|
206
|
+
// Prisma unique constraint violation
|
|
207
|
+
if (err.constructor.name === 'PrismaClientKnownRequestError' && (err as any).code === 'P2002') {
|
|
208
|
+
return res.status(409).json({ error: 'A record with this value already exists.' });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
logger.error({ err, url: req.url, method: req.method });
|
|
212
|
+
res.status(500).json({ error: 'Internal server error.' });
|
|
213
|
+
};
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Rate limiting
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
import rateLimit from 'express-rate-limit';
|
|
222
|
+
|
|
223
|
+
// General API rate limit
|
|
224
|
+
export const apiLimiter = rateLimit({
|
|
225
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
226
|
+
max: 100,
|
|
227
|
+
standardHeaders: true,
|
|
228
|
+
legacyHeaders: false,
|
|
229
|
+
message: { error: 'Too many requests. Please try again later.' },
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Strict limit for auth endpoints
|
|
233
|
+
export const authLimiter = rateLimit({
|
|
234
|
+
windowMs: 15 * 60 * 1000,
|
|
235
|
+
max: 5,
|
|
236
|
+
message: { error: 'Too many login attempts. Please try again in 15 minutes.' },
|
|
237
|
+
});
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## App setup
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
// src/app.ts
|
|
246
|
+
import express from 'express';
|
|
247
|
+
import cors from 'cors';
|
|
248
|
+
import helmet from 'helmet';
|
|
249
|
+
import compression from 'compression';
|
|
250
|
+
|
|
251
|
+
const app = express();
|
|
252
|
+
|
|
253
|
+
// Security
|
|
254
|
+
app.use(helmet());
|
|
255
|
+
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') }));
|
|
256
|
+
|
|
257
|
+
// Parsing
|
|
258
|
+
app.use(express.json({ limit: '10mb' }));
|
|
259
|
+
app.use(compression());
|
|
260
|
+
|
|
261
|
+
// Rate limiting
|
|
262
|
+
app.use('/api/', apiLimiter);
|
|
263
|
+
app.use('/api/auth/', authLimiter);
|
|
264
|
+
|
|
265
|
+
// Routes
|
|
266
|
+
app.use('/api/appointments', appointmentRoutes);
|
|
267
|
+
app.use('/api/auth', authRoutes);
|
|
268
|
+
|
|
269
|
+
// Health check
|
|
270
|
+
app.get('/health', (_, res) => res.json({ status: 'ok', uptime: process.uptime() }));
|
|
271
|
+
|
|
272
|
+
// Error handler — always last
|
|
273
|
+
app.use(errorHandler);
|
|
274
|
+
|
|
275
|
+
export { app };
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## Graceful shutdown
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
// src/server.ts
|
|
284
|
+
const server = app.listen(PORT, () => {
|
|
285
|
+
logger.info(`Server running on port ${PORT}`);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const shutdown = async (signal: string) => {
|
|
289
|
+
logger.info(`${signal} received — shutting down gracefully`);
|
|
290
|
+
server.close(async () => {
|
|
291
|
+
await db.$disconnect();
|
|
292
|
+
logger.info('Shutdown complete.');
|
|
293
|
+
process.exit(0);
|
|
294
|
+
});
|
|
295
|
+
setTimeout(() => process.exit(1), 10_000); // force exit after 10s
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
299
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## ALWAYS
|
|
305
|
+
- Separate routes, controllers, services, repositories
|
|
306
|
+
- Validate at the boundary with Zod before touching services
|
|
307
|
+
- Use typed domain error classes (`AppError`, `NotFoundError`, etc.)
|
|
308
|
+
- Register the error handler middleware last
|
|
309
|
+
- Use `next(err)` in controllers — never `res.status(500)` inline
|
|
310
|
+
- Graceful shutdown on SIGTERM/SIGINT
|
|
311
|
+
|
|
312
|
+
## NEVER
|
|
313
|
+
- Business logic in routes or controllers
|
|
314
|
+
- Raw `try/catch` without `next(err)` in async controllers
|
|
315
|
+
- `console.log` in production — use a structured logger (pino/winston)
|
|
316
|
+
- `process.env.*` without validation at startup
|
|
317
|
+
- Skip rate limiting on auth endpoints
|