@sylphx/flow 1.0.4 → 1.0.6
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 +12 -0
- package/assets/agents/coder.md +162 -0
- package/assets/agents/orchestrator.md +416 -0
- package/assets/agents/reviewer.md +192 -0
- package/assets/agents/writer.md +364 -0
- package/assets/icons/flow-notification-icon.png +0 -0
- package/assets/icons/flow-notification-icon.svg +17 -0
- package/assets/knowledge/data/sql.md +216 -0
- package/assets/knowledge/guides/saas-template.md +85 -0
- package/assets/knowledge/guides/system-prompt.md +344 -0
- package/assets/knowledge/guides/tech-stack.md +92 -0
- package/assets/knowledge/guides/ui-ux.md +44 -0
- package/assets/knowledge/stacks/nextjs-app.md +165 -0
- package/assets/knowledge/stacks/node-api.md +220 -0
- package/assets/knowledge/stacks/react-app.md +232 -0
- package/assets/knowledge/universal/deployment.md +109 -0
- package/assets/knowledge/universal/performance.md +121 -0
- package/assets/knowledge/universal/security.md +79 -0
- package/assets/knowledge/universal/testing.md +111 -0
- package/assets/output-styles/silent.md +23 -0
- package/assets/rules/code-standards.md +346 -0
- package/assets/rules/core.md +189 -0
- package/assets/slash-commands/commit.md +23 -0
- package/assets/slash-commands/context.md +112 -0
- package/assets/slash-commands/explain.md +35 -0
- package/assets/slash-commands/review.md +39 -0
- package/assets/slash-commands/test.md +30 -0
- package/package.json +4 -2
- package/src/commands/flow-orchestrator.ts +17 -10
- package/src/core/state-detector.ts +11 -2
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Tech Stack (Recommended)
|
|
3
|
+
description: Opinionated stack (Next.js, PandaCSS, GraphQL, Pothos, Drizzle) - optimized for LLM accuracy
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Technical Stack
|
|
7
|
+
|
|
8
|
+
Scalable, secure SaaS stack. Type safety, performance, serverless. Validate with E2E (Playwright), monitor with Sentry.
|
|
9
|
+
|
|
10
|
+
## Domain-Driven Architecture
|
|
11
|
+
|
|
12
|
+
Feature-based layout: `src/features/<domain>/` (frontend), `src/graphql/<domain>/` (backend). Colocate related code. Cross-domain via explicit exports.
|
|
13
|
+
|
|
14
|
+
**Frontend domains**: `src/features/<domain>/` → `components/`, `hooks/`, `store/`, `services/`, `utils/`, `types.ts`
|
|
15
|
+
**Backend domains**: `src/graphql/<domain>/` → `types/`, `queries.ts`, `mutations.ts`, `subscriptions.ts`, `loaders.ts` (DataLoader for N+1)
|
|
16
|
+
**Shared infra**: `src/lib/` (clients), `src/app/` (routes, providers)
|
|
17
|
+
|
|
18
|
+
## Frontend Stack
|
|
19
|
+
|
|
20
|
+
**Framework**: Next.js App Router (routing, SSR/SSG, Turbopack). `src/app/(app)/dashboard/page.tsx`
|
|
21
|
+
|
|
22
|
+
**UI**: React + Radix UI primitives (a11y). Prefer Radix for structural/interactive, custom only when Radix lacks. `src/features/<domain>/components/`
|
|
23
|
+
|
|
24
|
+
**State**: Zustand (global sessions). Avoid Redux. `src/features/<domain>/store/`
|
|
25
|
+
|
|
26
|
+
**Styling**: PandaCSS (type-safe atomic CSS-in-JS, zero-runtime, <10KB)
|
|
27
|
+
- **Tokens/Themes**: `panda.config.ts` semantics (`colors.primary.500`), `_dark`/CSS vars
|
|
28
|
+
- **Atomic**: Inline JSX (`bg-primary-500 p-4`), `css({ color: 'red' })` merges
|
|
29
|
+
- **Recipes**: `cva` (Button variants), `sva` slots (Card), `jsx: ['Button']` track
|
|
30
|
+
- **Merging**: `cx(recipe(), css({ bg: 'red' }))` overrides
|
|
31
|
+
- **Optimize**: `staticCss: { recipes: '*' }`, purge globs, `panda analyze`, Next.js plugins
|
|
32
|
+
|
|
33
|
+
**Hooks**: react-use (localStorage, useMeasure, useDebounce, sensors), urql (GraphQL cache, SSR, subscriptions, offline, batching)
|
|
34
|
+
|
|
35
|
+
**Auth**: Better Auth (passkey-first, 2FA), reCAPTCHA (bot mitigation)
|
|
36
|
+
|
|
37
|
+
## Backend Stack
|
|
38
|
+
|
|
39
|
+
GraphQL-first, serverless. `src/graphql/<domain>/`
|
|
40
|
+
|
|
41
|
+
**Schema/Server**: Pothos (code-first), Yoga. `gql.tada` for all GraphQL docs (never raw templates), `graphql-scalars` for custom scalars
|
|
42
|
+
- Modular `queryField`, typed client hooks via `gql.tada`, colocate operations with components/pages, DataLoader in `loaders.ts` (batch, cache, prevent N+1)
|
|
43
|
+
|
|
44
|
+
**Auth**: Better Auth (JWT/Redis denylist), rotate tokens
|
|
45
|
+
|
|
46
|
+
**Request Context**: AsyncLocalStorage with `headers()`/`cookies()` → tiny accessors (`getAuthSession()`, `getLocale()`) instead of passing context objects
|
|
47
|
+
|
|
48
|
+
**ORM**: Drizzle (queries/migrations). **Never** raw SQL except unavoidable complex cases (use parameterized placeholders). Query builder methods (`eq`, `and`, `or`). Schemas/queries per domain: `src/domains/<domain>/data/`
|
|
49
|
+
- `db.select().from(users).where(eq(users.id, userId))`
|
|
50
|
+
|
|
51
|
+
**Security**: @simplewebauthn/server, Redis limits
|
|
52
|
+
|
|
53
|
+
## Data Layer
|
|
54
|
+
|
|
55
|
+
**DB**: PostgreSQL (Neon/pgBouncer), RLS. Scale: Partition (logs/date)
|
|
56
|
+
|
|
57
|
+
**Cache/RT**: Upstash Redis (cache/pubsub/streams). TTL (24h), event streams
|
|
58
|
+
|
|
59
|
+
## Payments
|
|
60
|
+
|
|
61
|
+
**Billing**: Stripe (Checkout/Portal/Invoices/webhooks idempotent). Wallet credit on session complete, 3x retry
|
|
62
|
+
|
|
63
|
+
## DevOps
|
|
64
|
+
|
|
65
|
+
**Local**: Docker Compose (stack), bun, Biome (linting/formatting), Lefthook (Git hooks)
|
|
66
|
+
- **Biome Ignore**: Tests (`__tests__/**`, `*.test.*`), generated (`*.generated.*`, `dist/**`), project-specific (`styled-system`, `*.gql.tada.ts`, `drizzle`, `.next`)
|
|
67
|
+
- **Biome Config**: Recommended + custom flow, ignoreUnknown false
|
|
68
|
+
- **Lefthook**: Pre-commit (Biome, type-check), pre-push (tests). `bun add -D lefthook`, `lefthook install`
|
|
69
|
+
- **Entry**: `bun install && migrate && dev`, Lefthook auto-runs, `biome check .` in CI
|
|
70
|
+
|
|
71
|
+
**Deploy**: Serverless (Vercel), GraphQL BFF. CI: Actions, 99.9% SLO alerts
|
|
72
|
+
|
|
73
|
+
## Framework Rules
|
|
74
|
+
|
|
75
|
+
### GraphQL Standards
|
|
76
|
+
- **IDs**: Use `ID` scalar (not `String`). Base64 for keys.
|
|
77
|
+
- **Enums/Unions**: Enums for fixed (Role), unions for polymorphic (Result = Post | User). Limit depth 3-5.
|
|
78
|
+
|
|
79
|
+
### GraphQL Document Placement
|
|
80
|
+
Colocate operations in domain services (`src/features/<domain>/services/`), `src/graphql/<domain>/`
|
|
81
|
+
- **Routes/pages**: Import from domain services for tree-shaking
|
|
82
|
+
- **Components**: Queries/mutations in domain services, export typed helpers
|
|
83
|
+
- **Stores**: GraphQL docs in domain services
|
|
84
|
+
- **Fragments**: `src/features/<domain>/services/fragments/` with barrel exports
|
|
85
|
+
- **Tests**: Colocate under `src/features/<domain>/`
|
|
86
|
+
- **Typed modules**: `.gql.ts` stay domain-local, enriched by `graphql-env.d.ts`
|
|
87
|
+
|
|
88
|
+
### Pothos Best Practices
|
|
89
|
+
- **ID Handling**: `exposeId: false` by default (security), `exposeId: true` only when needed
|
|
90
|
+
- **Subscription Ordering**: `subscribe` before `resolve` for TS inference
|
|
91
|
+
- **Extensions**: `extendType` for modularity, integrate gql.tada for E2E types
|
|
92
|
+
- **Errors**: Custom scalars (graphql-scalars), try-catch with GraphQLError
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: UI/UX Guidelines
|
|
3
|
+
description: Modern SPA design patterns, PandaCSS, Radix UI, conversational interfaces
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Modern SPA UI/UX
|
|
7
|
+
|
|
8
|
+
Apply when designing/updating single-page application interfaces. Keep concise, modular, consistent with conversational products (chat-first SaaS).
|
|
9
|
+
|
|
10
|
+
## Core Principles
|
|
11
|
+
- Minimalist functionalism: Remove clutter, highlight primary content/controls
|
|
12
|
+
- Balanced readability: Letter-spacing 1.2–1.5, line-height ≥1.5, clear hierarchy
|
|
13
|
+
- Global consistency: Reuse tokens for spacing, color, typography, radii
|
|
14
|
+
- Perceived fluidity: Transitions <200ms, never block input
|
|
15
|
+
|
|
16
|
+
## Visual System
|
|
17
|
+
- **Color**: Dark backgrounds (#111–#1A1A), optional light mode. One accent for CTAs. WCAG AA contrast ≥4.5:1
|
|
18
|
+
- **Shapes**: 8–12px border radius. Cards with subtle shadows (0 1px 3px rgba(0,0,0,0.1))
|
|
19
|
+
- **Typography**: Sans-serif (Inter, SF Pro). Bold headings, progressively larger. Body 14–16px, 1.5 line-height
|
|
20
|
+
|
|
21
|
+
## Micro-Interactions
|
|
22
|
+
- Duration: 200–400ms ease-in-out
|
|
23
|
+
- Hover: 1.03× scale or deeper shadow
|
|
24
|
+
- Active: 0.95–0.98× for tactile feedback
|
|
25
|
+
- Page transitions: Fade/slide, prefetch routes (<100ms latency)
|
|
26
|
+
|
|
27
|
+
## Interaction Patterns
|
|
28
|
+
- **Conversational**: Messages top-to-bottom, timestamp/metadata aligned (chat UIs)
|
|
29
|
+
- **Input**: Pin primary composer at viewport bottom, minimal controls
|
|
30
|
+
- **Feedback**: Visual response on every submit/load/state change (spinners, progress, confirmations)
|
|
31
|
+
- **Expandable**: Collapse long sections, keep critical details visible
|
|
32
|
+
|
|
33
|
+
## UX Guidelines
|
|
34
|
+
- Persistent nav (top/sidebar), limit to 5–7 items
|
|
35
|
+
- Eliminate distractions: No autoplay, reduce focal points
|
|
36
|
+
- Natural scrolling, lazy loading for long lists/media
|
|
37
|
+
- Reusable primitives (buttons, cards, inputs) with consistent props
|
|
38
|
+
- Mobile-first: Validate 320px, 768px, 1024px breakpoints, identical functionality
|
|
39
|
+
|
|
40
|
+
## Validation
|
|
41
|
+
- Contrast/readability in dark/light (axe-core, Lighthouse ≥90)
|
|
42
|
+
- Hover/tap feedback within 50ms
|
|
43
|
+
- Responsive layouts on phone/tablet/desktop, adjust spacing/typography for overflow
|
|
44
|
+
- Document UI decisions (tokens, components) for reuse
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Next.js Application
|
|
3
|
+
description: Next.js App Router, Server Components, data fetching, routing, deployment
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Next.js Development
|
|
7
|
+
|
|
8
|
+
## When Next.js vs React SPA
|
|
9
|
+
|
|
10
|
+
**Next.js**: SEO matters, fast initial load, full-stack in one codebase
|
|
11
|
+
**React SPA**: Dashboard/admin, behind auth, separate backend exists
|
|
12
|
+
|
|
13
|
+
## App Router vs Pages Router
|
|
14
|
+
|
|
15
|
+
**App Router (app/)** - Recommended:
|
|
16
|
+
- Server Components by default (less JS)
|
|
17
|
+
- Built-in layouts, loading, error states
|
|
18
|
+
- Better data fetching patterns
|
|
19
|
+
|
|
20
|
+
**Pages Router (pages/)**: Legacy maintenance only
|
|
21
|
+
|
|
22
|
+
## Server vs Client Components
|
|
23
|
+
|
|
24
|
+
### Decision Tree
|
|
25
|
+
```
|
|
26
|
+
Needs interactivity? (onClick, useState, hooks)
|
|
27
|
+
├─ YES → 'use client'
|
|
28
|
+
└─ NO → Server Component (default)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Server Components (default):**
|
|
32
|
+
- Render on server, direct DB access, zero client JS
|
|
33
|
+
- Can't use hooks/handlers
|
|
34
|
+
|
|
35
|
+
**Client Components ('use client'):**
|
|
36
|
+
- Hooks, handlers, interactivity, ships JS to browser
|
|
37
|
+
|
|
38
|
+
### Composition Pattern
|
|
39
|
+
Server components wrap client components. Fetch data in server, pass as props to client.
|
|
40
|
+
|
|
41
|
+
## Data Fetching
|
|
42
|
+
|
|
43
|
+
### Cache Options
|
|
44
|
+
```typescript
|
|
45
|
+
// Cached forever (default)
|
|
46
|
+
await fetch('https://api.example.com/data')
|
|
47
|
+
|
|
48
|
+
// Cache with revalidation
|
|
49
|
+
await fetch('...', { next: { revalidate: 3600 } })
|
|
50
|
+
|
|
51
|
+
// Never cache
|
|
52
|
+
await fetch('...', { cache: 'no-store' })
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Parallel Fetching
|
|
56
|
+
```typescript
|
|
57
|
+
// BAD - Sequential
|
|
58
|
+
const posts = await getPosts()
|
|
59
|
+
const users = await getUsers()
|
|
60
|
+
|
|
61
|
+
// GOOD - Parallel
|
|
62
|
+
const [posts, users] = await Promise.all([getPosts(), getUsers()])
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Caching & Revalidation
|
|
66
|
+
|
|
67
|
+
**Time-based (ISR):**
|
|
68
|
+
```typescript
|
|
69
|
+
export const revalidate = 3600 // Every hour
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**On-demand:**
|
|
73
|
+
```typescript
|
|
74
|
+
import { revalidatePath, revalidateTag } from 'next/cache'
|
|
75
|
+
revalidatePath('/posts')
|
|
76
|
+
revalidateTag('posts')
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Dynamic (no cache):**
|
|
80
|
+
```typescript
|
|
81
|
+
export const dynamic = 'force-dynamic'
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Routing
|
|
85
|
+
|
|
86
|
+
### File Structure
|
|
87
|
+
```
|
|
88
|
+
app/page.tsx → /
|
|
89
|
+
app/about/page.tsx → /about
|
|
90
|
+
app/blog/[slug]/page.tsx → /blog/:slug
|
|
91
|
+
app/(marketing)/features/page.tsx → /features (route group)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Special Files
|
|
95
|
+
- `layout.tsx`: Shared UI for segment + children
|
|
96
|
+
- `loading.tsx`: Loading UI (Suspense boundary)
|
|
97
|
+
- `error.tsx`: Error UI (must be 'use client')
|
|
98
|
+
|
|
99
|
+
## Authentication
|
|
100
|
+
|
|
101
|
+
### Middleware (Route Protection)
|
|
102
|
+
```typescript
|
|
103
|
+
// middleware.ts
|
|
104
|
+
export function middleware(request: Request) {
|
|
105
|
+
const token = request.cookies.get('token')
|
|
106
|
+
if (!token) return NextResponse.redirect(new URL('/login', request.url))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const config = { matcher: '/dashboard/:path*' }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Server Actions (Form Handling)
|
|
113
|
+
```typescript
|
|
114
|
+
// app/actions.ts
|
|
115
|
+
'use server'
|
|
116
|
+
|
|
117
|
+
export async function createPost(formData: FormData) {
|
|
118
|
+
const title = formData.get('title')
|
|
119
|
+
if (!title) return { error: 'Missing title' }
|
|
120
|
+
|
|
121
|
+
const post = await db.posts.create({ data: { title } })
|
|
122
|
+
revalidatePath('/posts')
|
|
123
|
+
return { success: true, post }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// In component
|
|
127
|
+
<form action={createPost}>
|
|
128
|
+
<input name="title" />
|
|
129
|
+
<button type="submit">Create</button>
|
|
130
|
+
</form>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Performance
|
|
134
|
+
|
|
135
|
+
**Image**: Use `<Image>` with `priority` for above-fold, `placeholder="blur"` for UX
|
|
136
|
+
**Font**: `next/font/google` for automatic optimization
|
|
137
|
+
**Code Splitting**: `dynamic(() => import('./Heavy'), { ssr: false })` for client-only heavy components
|
|
138
|
+
|
|
139
|
+
## Common Pitfalls
|
|
140
|
+
|
|
141
|
+
❌ **'use client' everywhere** → Only for interactivity
|
|
142
|
+
❌ **Over-fetching** → Fetch once in layout/page, pass as props
|
|
143
|
+
❌ **Not streaming** → Use Suspense boundaries
|
|
144
|
+
❌ **Forgetting revalidation** → Always revalidate after mutations
|
|
145
|
+
|
|
146
|
+
## Environment Variables
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
DATABASE_URL="..." # Server only
|
|
150
|
+
NEXT_PUBLIC_API_URL="..." # Exposed to browser (must start with NEXT_PUBLIC_)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Decision Guide
|
|
154
|
+
|
|
155
|
+
**Server Component:**
|
|
156
|
+
- Fetching data, backend access, large dependencies, non-interactive
|
|
157
|
+
|
|
158
|
+
**Client Component:**
|
|
159
|
+
- Interactive (clicks, state, hooks), browser APIs, event handlers
|
|
160
|
+
|
|
161
|
+
**API Route:**
|
|
162
|
+
- Hide API keys, webhooks, complex server logic, third-party integrations
|
|
163
|
+
|
|
164
|
+
**Server Action:**
|
|
165
|
+
- Form submissions, mutations (CRUD), simple server ops
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Node.js API
|
|
3
|
+
description: Express/Fastify, REST/GraphQL, authentication, middleware, error handling
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Backend API Development
|
|
7
|
+
|
|
8
|
+
## REST Structure
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
GET /users List
|
|
12
|
+
POST /users Create
|
|
13
|
+
GET /users/:id Get
|
|
14
|
+
PATCH /users/:id Update
|
|
15
|
+
DELETE /users/:id Delete
|
|
16
|
+
GET /users/:id/posts Nested
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**Status**: 200 OK, 201 Created, 204 No Content, 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 500 Internal Server Error
|
|
20
|
+
|
|
21
|
+
**Response format (project standard):**
|
|
22
|
+
```json
|
|
23
|
+
{ "data": {...}, "meta": {...} }
|
|
24
|
+
{ "items": [...], "total": 100, "page": 1, "limit": 20 }
|
|
25
|
+
{ "error": { "code": "VALIDATION_ERROR", "message": "...", "field": "..." } }
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## N+1 Problem
|
|
29
|
+
|
|
30
|
+
```javascript
|
|
31
|
+
// BAD - N+1 queries
|
|
32
|
+
for (user of users) { user.posts = await getPosts(user.id) }
|
|
33
|
+
|
|
34
|
+
// GOOD - Join
|
|
35
|
+
await db.users.findMany({ include: { posts: true } })
|
|
36
|
+
|
|
37
|
+
// GOOD - Batch fetch
|
|
38
|
+
const userIds = users.map(u => u.id)
|
|
39
|
+
const posts = await db.posts.findMany({ where: { userId: { in: userIds } } })
|
|
40
|
+
|
|
41
|
+
// GraphQL: DataLoader
|
|
42
|
+
const loader = new DataLoader(async (ids) => {
|
|
43
|
+
const items = await db.find({ where: { id: { in: ids } } })
|
|
44
|
+
return ids.map(id => items.filter(i => i.parentId === id))
|
|
45
|
+
})
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Database
|
|
49
|
+
|
|
50
|
+
**Connection pooling:**
|
|
51
|
+
```javascript
|
|
52
|
+
const pool = new Pool({ max: 20, idleTimeoutMillis: 30000 })
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Caching pattern:**
|
|
56
|
+
```javascript
|
|
57
|
+
async function getUser(id) {
|
|
58
|
+
const cached = await redis.get(`user:${id}`)
|
|
59
|
+
if (cached) return JSON.parse(cached)
|
|
60
|
+
|
|
61
|
+
const user = await db.users.findUnique({ where: { id } })
|
|
62
|
+
await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 3600)
|
|
63
|
+
return user
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Invalidate on update
|
|
67
|
+
async function updateUser(id, data) {
|
|
68
|
+
const user = await db.users.update({ where: { id }, data })
|
|
69
|
+
await redis.del(`user:${id}`)
|
|
70
|
+
return user
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Authentication
|
|
75
|
+
|
|
76
|
+
**Session vs Token:**
|
|
77
|
+
- Session: Traditional web apps, need fine-grained control
|
|
78
|
+
- Token (JWT): SPAs, mobile apps, microservices
|
|
79
|
+
|
|
80
|
+
**JWT middleware (project standard):**
|
|
81
|
+
```javascript
|
|
82
|
+
function requireAuth(req, res, next) {
|
|
83
|
+
const token = req.headers.authorization?.split(' ')[1]
|
|
84
|
+
if (!token) return res.status(401).json({ error: { code: 'NO_TOKEN' } })
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
req.userId = jwt.verify(token, process.env.JWT_SECRET).userId
|
|
88
|
+
next()
|
|
89
|
+
} catch {
|
|
90
|
+
res.status(401).json({ error: { code: 'INVALID_TOKEN' } })
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Authorization patterns:**
|
|
96
|
+
```javascript
|
|
97
|
+
// Role check
|
|
98
|
+
function requireRole(...roles) {
|
|
99
|
+
return async (req, res, next) => {
|
|
100
|
+
const user = await db.users.findUnique({ where: { id: req.userId } })
|
|
101
|
+
if (!roles.includes(user.role)) {
|
|
102
|
+
return res.status(403).json({ error: { code: 'FORBIDDEN' } })
|
|
103
|
+
}
|
|
104
|
+
next()
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Ownership check
|
|
109
|
+
function requireOwnership(getter) {
|
|
110
|
+
return async (req, res, next) => {
|
|
111
|
+
const resource = await getter(req)
|
|
112
|
+
if (resource.userId !== req.userId) {
|
|
113
|
+
return res.status(403).json({ error: { code: 'NOT_OWNER' } })
|
|
114
|
+
}
|
|
115
|
+
next()
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Error Handling
|
|
121
|
+
|
|
122
|
+
**Project standard:**
|
|
123
|
+
```javascript
|
|
124
|
+
class ApiError extends Error {
|
|
125
|
+
constructor(statusCode, code, message) {
|
|
126
|
+
super(message)
|
|
127
|
+
this.statusCode = statusCode
|
|
128
|
+
this.code = code
|
|
129
|
+
this.isOperational = true
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Error middleware (last!)
|
|
134
|
+
app.use((err, req, res, next) => {
|
|
135
|
+
if (err.isOperational) {
|
|
136
|
+
return res.status(err.statusCode).json({
|
|
137
|
+
error: { code: err.code, message: err.message }
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
console.error('PROGRAMMER ERROR:', err)
|
|
142
|
+
res.status(500).json({ error: { code: 'INTERNAL_ERROR' } })
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
// Usage
|
|
146
|
+
if (!user) throw new ApiError(404, 'NOT_FOUND', 'User not found')
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Performance
|
|
150
|
+
|
|
151
|
+
**Targets**: DB < 100ms, API < 200ms
|
|
152
|
+
|
|
153
|
+
**Optimize:**
|
|
154
|
+
- N+1 → Joins / DataLoader
|
|
155
|
+
- Connection pooling
|
|
156
|
+
- Redis caching
|
|
157
|
+
- Job queues (background tasks)
|
|
158
|
+
- Pagination (always LIMIT)
|
|
159
|
+
|
|
160
|
+
## GraphQL Basics
|
|
161
|
+
|
|
162
|
+
**When**: Complex relationships, flexible queries
|
|
163
|
+
**vs REST**: Simple CRUD → REST
|
|
164
|
+
|
|
165
|
+
**DataLoader (required for N+1):**
|
|
166
|
+
```javascript
|
|
167
|
+
const postLoader = new DataLoader(async (userIds) => {
|
|
168
|
+
const posts = await db.posts.findMany({ where: { userId: { in: userIds } } })
|
|
169
|
+
return userIds.map(id => posts.filter(p => p.userId === id))
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
// In resolver
|
|
173
|
+
User: {
|
|
174
|
+
posts: (user) => postLoader.load(user.id)
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Common Patterns
|
|
179
|
+
|
|
180
|
+
**Repository:**
|
|
181
|
+
```javascript
|
|
182
|
+
class UserRepo {
|
|
183
|
+
findById(id) { return db.users.findUnique({ where: { id } }) }
|
|
184
|
+
save(data) { return db.users.create({ data }) }
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Service:**
|
|
189
|
+
```javascript
|
|
190
|
+
class UserService {
|
|
191
|
+
async createUser(data) {
|
|
192
|
+
if (!data.email) throw new Error('Email required')
|
|
193
|
+
return await this.repo.save(data)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Middleware chain:**
|
|
199
|
+
```javascript
|
|
200
|
+
app.use(cors())
|
|
201
|
+
app.use(express.json())
|
|
202
|
+
app.use(rateLimit())
|
|
203
|
+
app.use(auth())
|
|
204
|
+
app.use(errorHandler()) // Last!
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Best Practices
|
|
208
|
+
|
|
209
|
+
✅ Prepared statements (prevent injection)
|
|
210
|
+
✅ Connection pooling
|
|
211
|
+
✅ Index foreign keys
|
|
212
|
+
✅ Rate limit auth endpoints
|
|
213
|
+
✅ Hash passwords (bcrypt/argon2)
|
|
214
|
+
✅ HTTPS only
|
|
215
|
+
✅ Validate server-side
|
|
216
|
+
|
|
217
|
+
❌ SQL injection (string concat)
|
|
218
|
+
❌ Plain text passwords
|
|
219
|
+
❌ N+1 queries
|
|
220
|
+
❌ No error handling
|