rbin-task-flow 1.3.0 → 1.5.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.
@@ -0,0 +1,712 @@
1
+ ---
2
+ description: Coding standards and architecture patterns for all projects
3
+ globs: **/*
4
+ alwaysApply: true
5
+ ---
6
+
7
+ # Coding Standards & Architecture
8
+
9
+ These rules define how code MUST be written when implementing tasks. Always follow these patterns without deviation.
10
+
11
+ ---
12
+
13
+ ## Project Structure
14
+
15
+ All projects use TypeScript and follow this structure:
16
+
17
+ ```
18
+ src/
19
+ ├── app/ # Route definitions only (Next.js App Router or Expo Router)
20
+ ├── features/ # Business domain logic, organized by feature
21
+ └── shared/ # Global reusable code
22
+ ```
23
+
24
+ ---
25
+
26
+ ## Tech Stack
27
+
28
+ ### Front-end (Next.js)
29
+ - **Framework**: Next.js 15+ with App Router
30
+ - **Language**: TypeScript (strict mode)
31
+ - **Styling**: Tailwind CSS v4 + `clsx` + `tailwind-merge` via `cn()` helper
32
+ - **UI Components**: shadcn/ui + `lucide-react` icons
33
+ - **Data Fetching**: `@tanstack/react-query`
34
+ - **Forms**: `react-hook-form` + `zod` + `@hookform/resolvers/zod`
35
+ - **Notifications**: `sonner`
36
+ - **ESLint**: `@rbinflow/eslint-config`
37
+ - **E2E Testing**: Playwright and/or Cypress
38
+
39
+ ### Mobile (Expo / React Native)
40
+ - **Framework**: Expo with Expo Router
41
+ - **Language**: TypeScript (strict mode)
42
+ - **Styling**: NativeWind + `clsx` + `tailwind-merge` via `cn()` helper
43
+ - **Data Fetching**: `@tanstack/react-query`
44
+ - **Forms**: `react-hook-form` + `zod` + `@hookform/resolvers/zod`
45
+ - **Build**: EAS (Expo Application Services)
46
+ - **ESLint**: `@rbinflow/eslint-config`
47
+ - **E2E Testing**: Detox
48
+
49
+ ### Backend (NestJS)
50
+ - **Framework**: NestJS 10+
51
+ - **Language**: TypeScript (strict mode)
52
+ - **ORM**: Prisma
53
+ - **Database**: PostgreSQL
54
+ - **Auth**: JWT + Passport
55
+ - **Validation**: Zod (not class-validator/class-transformer)
56
+ - **Structure**: `src/app/` (controllers), `src/features/` (use-cases, repositories), `src/shared/` (guards, pipes, gateways)
57
+
58
+ ---
59
+
60
+ ## app/ — Routes Only (Front-end & Mobile) / Controllers Only (Backend)
61
+
62
+ The `app/` directory contains ONLY route definitions. Each file is a thin wrapper that imports and renders the feature page/screen.
63
+
64
+ **Front-end (Next.js App Router):**
65
+ ```typescript
66
+ // src/app/(private)/dashboard/page.tsx
67
+ import { DashboardPage } from '@/features/dashboard/pages/dashboard.page'
68
+
69
+ export default function Dashboard() {
70
+ return <DashboardPage />
71
+ }
72
+ ```
73
+
74
+ **Mobile (Expo Router):**
75
+ ```typescript
76
+ // src/app/(private)/dashboard.tsx
77
+ import { DashboardScreen } from '@/features/dashboard/screens/dashboard.screen'
78
+
79
+ export default function Dashboard() {
80
+ return <DashboardScreen />
81
+ }
82
+ ```
83
+
84
+ **Backend (NestJS):**
85
+ ```typescript
86
+ // src/app/account/controllers/create-session.controller.ts
87
+ import { Body, Controller, HttpCode, Post } from '@nestjs/common'
88
+ import { CreateSessionBodySchema, createSessionValidator } from '@/app/account/validators/create-session.validator'
89
+ import { CreateSessionUseCase } from '@/features/account/use-cases/create-session.use-case'
90
+
91
+ @Controller('/session')
92
+ export class CreateSessionController {
93
+ constructor(private createSessionUseCase: CreateSessionUseCase) {}
94
+
95
+ @Post()
96
+ @HttpCode(200)
97
+ async handle(@Body(createSessionValidator) data: CreateSessionBodySchema) {
98
+ return this.createSessionUseCase.execute(data)
99
+ }
100
+ }
101
+ ```
102
+
103
+ **Rules:**
104
+ - **Front-end/Mobile**: `page.tsx` / route files NEVER contain logic, state, or imports beyond the feature component
105
+ - **Backend**: `app/` contains controllers and validators only — no business logic
106
+ - Group routes with `(public)` and `(private)` folders (front/mobile)
107
+ - `layout.tsx` files may contain auth guards and providers
108
+ - All business logic lives in `features/`
109
+
110
+ ---
111
+
112
+ ## features/ — Feature Organization
113
+
114
+ Each feature is self-contained. Structure:
115
+
116
+ ```
117
+ features/[feature-name]/
118
+ ├── components/ # UI components specific to this feature
119
+ ├── hooks/ # Context providers and custom hooks
120
+ ├── pages/ # Page component (front-end) — orchestrator
121
+ ├── screens/ # Screen component (mobile) — orchestrator
122
+ ├── schemas/ # Zod validation schemas
123
+ ├── services/ # React Query wrappers (useQuery / useMutation)
124
+ ├── types/ # TypeScript types for this feature
125
+ ├── use-cases/ # Pure API call logic
126
+ └── utils/ # Feature-specific utilities (optional)
127
+ ```
128
+
129
+ ### Backend Feature Structure
130
+
131
+ ```
132
+ features/[feature-name]/
133
+ ├── use-cases/ # Business logic
134
+ │ └── create-account.use-case.ts
135
+ ├── repositories/ # Data access (abstract + Prisma implementation)
136
+ │ ├── account.repository.ts # Abstract class
137
+ │ └── prisma-account.repository.ts # Prisma implementation
138
+ ├── types/ # TypeScript types
139
+ │ └── account.type.ts
140
+ └── utils/ # Feature-specific utilities (optional)
141
+ ```
142
+
143
+ **Use Case (backend):**
144
+ ```typescript
145
+ // src/features/account/use-cases/create-session.use-case.ts
146
+ import { Injectable, UnauthorizedException } from '@nestjs/common'
147
+ import { AccountRepository } from '@/features/account/repositories/account.repository'
148
+ import { EncryptionGateway } from '@/shared/gateways/encryption.gateway'
149
+ import { JwtGateway } from '@/shared/gateways/jwt.gateway'
150
+
151
+ @Injectable()
152
+ export class CreateSessionUseCase {
153
+ constructor(
154
+ private accountRepository: AccountRepository,
155
+ private encryption: EncryptionGateway,
156
+ private jwt: JwtGateway,
157
+ ) {}
158
+
159
+ async execute(data: { email: string; password: string }) {
160
+ const account = await this.accountRepository.findByEmail(data.email)
161
+ if (!account) throw new UnauthorizedException('Invalid credentials')
162
+
163
+ const isValid = await this.encryption.validateHash({
164
+ value: data.password,
165
+ hashedValue: account.password,
166
+ })
167
+ if (!isValid) throw new UnauthorizedException('Invalid credentials')
168
+
169
+ return this.jwt.generateAuthTokens({ accountId: account.id })
170
+ }
171
+ }
172
+ ```
173
+
174
+ **Zod validation in NestJS (not class-validator):**
175
+ ```typescript
176
+ // src/app/account/validators/create-session.validator.ts
177
+ import { z } from 'zod'
178
+ import { ZodValidationPipe } from '@/shared/pipes/zod-validation.pipe'
179
+
180
+ const createSessionBodySchema = z.object({
181
+ email: z.string().email(),
182
+ password: z.string().min(6),
183
+ })
184
+
185
+ export type CreateSessionBodySchema = z.infer<typeof createSessionBodySchema>
186
+ export const createSessionValidator = new ZodValidationPipe(createSessionBodySchema)
187
+ ```
188
+
189
+ ```typescript
190
+ // src/shared/pipes/zod-validation.pipe.ts
191
+ import { BadRequestException, PipeTransform } from '@nestjs/common'
192
+ import { ZodError, ZodSchema } from 'zod'
193
+
194
+ export class ZodValidationPipe implements PipeTransform {
195
+ constructor(private schema: ZodSchema) {}
196
+
197
+ transform(value: unknown) {
198
+ try {
199
+ return this.schema.parse(value)
200
+ } catch (error) {
201
+ if (error instanceof ZodError) {
202
+ throw new BadRequestException({
203
+ message: 'Validation failed',
204
+ errors: error.errors.map((e) => ({ field: e.path.join('.'), message: e.message })),
205
+ })
206
+ }
207
+ throw new BadRequestException('Validation failed')
208
+ }
209
+ }
210
+ }
211
+ ```
212
+
213
+ **Gateways (abstract services in shared/):**
214
+ ```typescript
215
+ // src/shared/gateways/encryption.gateway.ts
216
+ export abstract class EncryptionGateway {
217
+ abstract createHash(value: string): Promise<string>
218
+ abstract validateHash(params: { value: string; hashedValue: string }): Promise<boolean>
219
+ }
220
+ ```
221
+
222
+ Gateways abstract external concerns (encryption, JWT, date, email). Implementations live alongside in `shared/gateways/` and are bound via NestJS modules.
223
+
224
+ ### Page / Screen — Orchestrator Pattern (Front-end/Mobile)
225
+
226
+ The page/screen is an orchestrator. It:
227
+ - Calls services (React Query hooks)
228
+ - Manages form state with `useForm`
229
+ - Passes data down to components as props
230
+ - Is NEVER a long file — delegates rendering to components
231
+
232
+ ```typescript
233
+ // src/features/dashboard/pages/dashboard.page.tsx
234
+ 'use client'
235
+
236
+ import { DashboardRevenueCard } from '@/features/dashboard/components/dashboard-revenue-card'
237
+ import { DashboardActiveCard } from '@/features/dashboard/components/dashboard-active-card'
238
+ import { GetDashboardActiveService } from '@/features/dashboard/services/get-dashboard-active.service'
239
+
240
+ export function DashboardPage() {
241
+ const { data: active, isLoading, isError } = GetDashboardActiveService()
242
+
243
+ return (
244
+ <section className="grid grid-cols-2 gap-5 my-10">
245
+ <DashboardActiveCard data={active} isLoading={isLoading} isError={isError} />
246
+ <DashboardRevenueCard />
247
+ </section>
248
+ )
249
+ }
250
+ ```
251
+
252
+ ### Components — Single Responsibility
253
+
254
+ Each component has a single responsibility. Components:
255
+ - Receive data via props (no direct service calls unless truly necessary)
256
+ - Are named after the feature: `[feature]-[description].tsx`
257
+ - Stay small and focused
258
+
259
+ ```typescript
260
+ // src/features/dashboard/components/dashboard-active-card.tsx
261
+ import { Card } from '@/shared/components/card'
262
+ import { Skeleton } from '@/shared/components/skeleton'
263
+
264
+ type DashboardActiveCardProps = {
265
+ data?: { count: number; trials: number }
266
+ isLoading: boolean
267
+ isError: boolean
268
+ }
269
+
270
+ export function DashboardActiveCard({ data, isLoading, isError }: DashboardActiveCardProps) {
271
+ if (isLoading) return <Skeleton className="h-[200px]" />
272
+ if (isError) return <ErrorState />
273
+
274
+ return (
275
+ <Card title="Active Members">
276
+ <p className="text-2xl font-bold">{data?.count}</p>
277
+ </Card>
278
+ )
279
+ }
280
+ ```
281
+
282
+ ---
283
+
284
+ ## shared/ — Global Reusable Code
285
+
286
+ ```
287
+ shared/
288
+ ├── components/
289
+ │ ├── button.tsx
290
+ │ ├── card.tsx
291
+ │ ├── data-handler.tsx
292
+ │ ├── skeleton.tsx
293
+ │ ├── dialog.tsx
294
+ │ └── form/
295
+ │ └── inputs/
296
+ │ ├── input-text.tsx
297
+ │ ├── input-select.tsx
298
+ │ └── shared/
299
+ │ ├── input-container.tsx
300
+ │ ├── input-error.tsx
301
+ │ └── input-label.tsx
302
+ ├── hooks/
303
+ │ ├── dialog.hook.tsx
304
+ │ └── drawer.hook.tsx
305
+ ├── libs/
306
+ │ ├── axios.ts
307
+ │ ├── react-query.ts
308
+ │ └── tw-merge.ts
309
+ ├── providers/
310
+ │ └── react-query-provider.tsx
311
+ ├── types/
312
+ │ └── shared.ui.type.ts
313
+ ├── constants/
314
+ │ └── server-routes.constants.ts
315
+ └── utils/
316
+ ├── validation/
317
+ │ ├── required-email.validation.ts
318
+ │ ├── required-string.validation.ts
319
+ │ └── required-phone.validation.ts
320
+ └── error.util.ts
321
+ ```
322
+
323
+ ---
324
+
325
+ ## DataHandler — Declarative Loading/Error States
326
+
327
+ Use a `DataHandler` component to handle loading, error, and empty states declaratively instead of repeating `if (isLoading)` / `if (isError)` in every component.
328
+
329
+ ```typescript
330
+ // src/shared/components/data-handler.tsx
331
+ import { ReactNode } from 'react'
332
+ import { Skeleton } from '@/shared/components/skeleton'
333
+
334
+ type DataHandlerProps = {
335
+ isLoading: boolean
336
+ isError: boolean
337
+ children: ReactNode
338
+ skeleton?: ReactNode
339
+ }
340
+
341
+ export function DataHandler({ isLoading, isError, children, skeleton }: DataHandlerProps) {
342
+ if (isLoading) return skeleton ?? <Skeleton />
343
+ if (isError) return <ErrorState />
344
+ return <>{children}</>
345
+ }
346
+ ```
347
+
348
+ Usage in pages:
349
+ ```typescript
350
+ <DataHandler isLoading={isLoading} isError={isError} skeleton={<DashboardSkeleton />}>
351
+ <DashboardRevenueCard revenue={revenue} />
352
+ </DataHandler>
353
+ ```
354
+
355
+ ---
356
+
357
+ ## Feature `platform` — Cross-Cutting Concerns
358
+
359
+ Every project has a special `features/platform/` feature for app-wide concerns that don't belong in `shared/` (they are app-specific, not reusable across projects):
360
+
361
+ ```
362
+ features/platform/
363
+ ├── components/ # App shell (menu, sidebar, header)
364
+ ├── constants/ # App routes, cookie keys, app name
365
+ │ ├── platform-routes.constants.ts
366
+ │ └── platform-cookies.constants.ts
367
+ ├── hooks/ # App-level hooks
368
+ ├── providers/ # AppProviders composition
369
+ │ └── app-providers-client.tsx
370
+ └── i18n/ # Internationalization (optional)
371
+ ```
372
+
373
+ ---
374
+
375
+ ## Providers Composition
376
+
377
+ Compose all providers in a dedicated `AppProviders` component instead of nesting them in `layout.tsx`:
378
+
379
+ ```typescript
380
+ // src/features/platform/providers/app-providers-client.tsx
381
+ 'use client'
382
+
383
+ import { ReactNode } from 'react'
384
+ import { Toaster } from 'sonner'
385
+ import { AuthProvider } from '@/features/auth/hooks/authentication.hook'
386
+ import { DialogProvider } from '@/shared/hooks/dialog.hook'
387
+
388
+ export function AppProvidersClient({ children }: { children: ReactNode }) {
389
+ return (
390
+ <AuthProvider>
391
+ <DialogProvider>
392
+ {children}
393
+ <Toaster position="top-right" />
394
+ </DialogProvider>
395
+ </AuthProvider>
396
+ )
397
+ }
398
+ ```
399
+
400
+ Then in the root layout:
401
+ ```typescript
402
+ // src/app/layout.tsx
403
+ export default function RootLayout({ children }: { children: ReactNode }) {
404
+ return (
405
+ <html lang="en">
406
+ <body>
407
+ <ReactQueryProvider>
408
+ <AppProvidersClient>{children}</AppProvidersClient>
409
+ </ReactQueryProvider>
410
+ </body>
411
+ </html>
412
+ )
413
+ }
414
+ ```
415
+
416
+ ---
417
+
418
+ ## cn() — Tailwind Class Merging
419
+
420
+ Always use `cn()` for class names. Never use raw string concatenation or template literals with conditional classes.
421
+
422
+ ```typescript
423
+ // src/shared/libs/tw-merge.ts
424
+ import { type ClassValue, clsx } from 'clsx'
425
+ import { twMerge } from 'tailwind-merge'
426
+
427
+ export function cn(...inputs: ClassValue[]) {
428
+ return twMerge(clsx(inputs))
429
+ }
430
+ ```
431
+
432
+ Usage:
433
+ ```typescript
434
+ import { cn } from '@/shared/libs/tw-merge'
435
+
436
+ className={cn(
437
+ 'flex flex-col rounded-lg bg-white px-5',
438
+ isActive && 'border-2 border-accent-500',
439
+ className,
440
+ )}
441
+ ```
442
+
443
+ ---
444
+
445
+ ## React Query — Service + Use Case Pattern
446
+
447
+ Split API calls into two layers:
448
+
449
+ **Use Case** — pure function, no React, just the API call:
450
+ ```typescript
451
+ // src/features/auth/use-cases/session-create.use-case.ts
452
+ import { api } from '@/shared/libs/axios'
453
+ import { serverRoutes } from '@/shared/constants/server-routes.constants'
454
+ import { LoginSchema } from '@/features/auth/schemas/login.schema'
455
+ import { SessionType } from '@/features/auth/types/session.api.type'
456
+
457
+ export async function sessionCreateUseCase(data: LoginSchema): Promise<SessionType> {
458
+ const response = await api.post(`${serverRoutes.session}/create`, data)
459
+ return response.data
460
+ }
461
+ ```
462
+
463
+ **Service** — React Query wrapper used in components:
464
+ ```typescript
465
+ // src/features/auth/services/session-create.service.tsx
466
+ 'use client'
467
+
468
+ import { useMutation } from '@tanstack/react-query'
469
+ import { sessionCreateUseCase } from '@/features/auth/use-cases/session-create.use-case'
470
+ import { SessionType } from '@/features/auth/types/session.api.type'
471
+ import { ServiceInput } from '@/shared/types/shared.ui.type'
472
+ import { handleError } from '@/shared/utils/error.util'
473
+
474
+ export function SessionCreateService({ onSuccess }: ServiceInput<SessionType>) {
475
+ return useMutation({
476
+ mutationFn: sessionCreateUseCase,
477
+ onSuccess: (data) => onSuccess?.(data),
478
+ onError: (error) => handleError(error),
479
+ })
480
+ }
481
+ ```
482
+
483
+ **Query service:**
484
+ ```typescript
485
+ // src/features/dashboard/services/get-dashboard-active.service.tsx
486
+ 'use client'
487
+
488
+ import { useQuery } from '@tanstack/react-query'
489
+ import { getDashboardActiveUseCase } from '@/features/dashboard/use-cases/get-dashboard-active.use-case'
490
+ import { DashboardActiveType } from '@/features/dashboard/types/dashboard-active.type'
491
+
492
+ export const getDashboardActiveQueryKey = 'getDashboardActiveQueryKey'
493
+
494
+ export function GetDashboardActiveService() {
495
+ return useQuery<DashboardActiveType>({
496
+ queryKey: [getDashboardActiveQueryKey],
497
+ queryFn: getDashboardActiveUseCase,
498
+ })
499
+ }
500
+ ```
501
+
502
+ **Shared service input type:**
503
+ ```typescript
504
+ // src/shared/types/shared.ui.type.ts
505
+ export type ServiceInput<TData = unknown> = {
506
+ onSuccess?: (data?: TData) => void
507
+ onError?: () => void
508
+ }
509
+ ```
510
+
511
+ ---
512
+
513
+ ## Forms — React Hook Form + Zod
514
+
515
+ **Schema:**
516
+ ```typescript
517
+ // src/features/auth/schemas/login.schema.ts
518
+ import { z } from 'zod'
519
+ import { requiredEmail } from '@/shared/utils/validation/required-email.validation'
520
+ import { requiredString } from '@/shared/utils/validation/required-string.validation'
521
+
522
+ export const loginSchema = z.object({
523
+ email: requiredEmail(),
524
+ password: requiredString({ field: 'password' }),
525
+ })
526
+
527
+ export type LoginSchema = z.infer<typeof loginSchema>
528
+ ```
529
+
530
+ **Reusable validators in shared/:**
531
+ ```typescript
532
+ // src/shared/utils/validation/required-string.validation.ts
533
+ import { z } from 'zod'
534
+
535
+ export const requiredString = ({ field, min = 1 }: { field: string; min?: number }) =>
536
+ z.string({ message: `${field} is required` }).min(min, {
537
+ message: `${field} must be at least ${min} characters`,
538
+ })
539
+ ```
540
+
541
+ **Form component:**
542
+ ```typescript
543
+ // src/features/auth/components/login-form.tsx
544
+ 'use client'
545
+
546
+ import { zodResolver } from '@hookform/resolvers/zod'
547
+ import { useForm } from 'react-hook-form'
548
+ import { LoginSchema, loginSchema } from '@/features/auth/schemas/login.schema'
549
+ import { SessionCreateService } from '@/features/auth/services/session-create.service'
550
+ import { InputText } from '@/shared/components/form/inputs/input-text'
551
+ import { Button } from '@/shared/components/button'
552
+
553
+ export function LoginForm() {
554
+ const { control, handleSubmit } = useForm<LoginSchema>({
555
+ resolver: zodResolver(loginSchema),
556
+ defaultValues: { email: '', password: '' },
557
+ })
558
+
559
+ const { mutate: createSession, isPending } = SessionCreateService({
560
+ onSuccess: () => { /* redirect */ },
561
+ })
562
+
563
+ return (
564
+ <form onSubmit={handleSubmit((data) => createSession(data))} className="flex flex-col gap-4">
565
+ <InputText name="email" control={control} placeholder="Email" />
566
+ <InputText name="password" control={control} type="password" placeholder="Password" />
567
+ <Button type="submit" isLoading={isPending}>Sign in</Button>
568
+ </form>
569
+ )
570
+ }
571
+ ```
572
+
573
+ **Input uses `Controller` from RHF and is generic:**
574
+ ```typescript
575
+ // src/shared/components/form/inputs/input-text.tsx
576
+ import { Control, Controller, FieldValues, Path, PathValue } from 'react-hook-form'
577
+ import { cn } from '@/shared/libs/tw-merge'
578
+
579
+ type InputTextProps<T extends FieldValues> = {
580
+ name: Path<T>
581
+ control: Control<T>
582
+ defaultValue?: PathValue<T, Path<T>>
583
+ placeholder?: string
584
+ type?: 'text' | 'password' | 'email'
585
+ disabled?: boolean
586
+ className?: string
587
+ }
588
+
589
+ export function InputText<T extends FieldValues>({
590
+ name, control, defaultValue = '' as PathValue<T, Path<T>>,
591
+ placeholder, type = 'text', disabled, className,
592
+ }: InputTextProps<T>) {
593
+ return (
594
+ <Controller
595
+ name={name}
596
+ control={control}
597
+ defaultValue={defaultValue}
598
+ render={({ field, fieldState: { error } }) => (
599
+ <div className={cn('flex flex-col gap-1', className)}>
600
+ <input
601
+ {...field}
602
+ type={type}
603
+ placeholder={placeholder}
604
+ disabled={disabled}
605
+ className={cn('border rounded-lg px-4 h-12 text-sm', error && 'border-red-500')}
606
+ />
607
+ {error && <span className="text-xs text-red-500">{error.message}</span>}
608
+ </div>
609
+ )}
610
+ />
611
+ )
612
+ }
613
+ ```
614
+
615
+ ---
616
+
617
+ ## File Naming Conventions
618
+
619
+ Use kebab-case for all filenames. Suffix reflects the file's role:
620
+
621
+ | Role | Suffix | Example |
622
+ |------|--------|---------|
623
+ | Page component | `.page.tsx` | `dashboard.page.tsx` |
624
+ | Screen component (mobile) | `.screen.tsx` | `login.screen.tsx` |
625
+ | UI component | `.tsx` | `dashboard-revenue-card.tsx` |
626
+ | React Query service | `.service.tsx` | `session-create.service.tsx` |
627
+ | API call (pure) | `.use-case.ts` | `session-create.use-case.ts` |
628
+ | Zod schema | `.schema.ts` | `login.schema.ts` |
629
+ | Context/hook | `.hook.tsx` | `authentication.hook.tsx` |
630
+ | API response type | `.api.type.ts` | `session.api.type.ts` |
631
+ | Domain type | `.type.ts` | `dashboard-active.type.ts` |
632
+ | Utility | `.util.ts` | `error.util.ts` |
633
+ | Validation helper | `.validation.ts` | `required-email.validation.ts` |
634
+ | Constants | `.constants.ts` | `server-routes.constants.ts` |
635
+
636
+ ---
637
+
638
+ ## TypeScript Types
639
+
640
+ - API response types: `[Name]Type` in a `.api.type.ts` file
641
+ - Domain/internal types: `[Name]Type` in a `.type.ts` file
642
+ - All types exported with `export type`
643
+ - No `any` — ever
644
+
645
+ ```typescript
646
+ // src/features/auth/types/session.api.type.ts
647
+ export type SessionType = {
648
+ accessToken: string
649
+ refreshToken: string
650
+ }
651
+ ```
652
+
653
+ ---
654
+
655
+ ## Context / Auth Hook Pattern
656
+
657
+ ```typescript
658
+ // src/features/auth/hooks/authentication.hook.tsx
659
+ 'use client'
660
+
661
+ import { createContext, useContext, ReactNode } from 'react'
662
+
663
+ type AuthContextData = {
664
+ account: AccountType | null
665
+ isAuthenticated: boolean
666
+ signOut: () => Promise<void>
667
+ }
668
+
669
+ const AuthContext = createContext<AuthContextData>({} as AuthContextData)
670
+
671
+ export function AuthProvider({ children }: { children: ReactNode }) {
672
+ // state and derived values here via useMemo
673
+ return (
674
+ <AuthContext.Provider value={{ account, isAuthenticated, signOut }}>
675
+ {children}
676
+ </AuthContext.Provider>
677
+ )
678
+ }
679
+
680
+ export const useAuth = () => useContext(AuthContext)
681
+ ```
682
+
683
+ ---
684
+
685
+ ## Testing
686
+
687
+ ### Front-end (Next.js)
688
+ - **E2E**: Playwright and/or Cypress
689
+ - Tests live in `e2e/` or `cypress/` at project root
690
+ - Test files: `[feature].spec.ts` (Playwright) or `[feature].cy.ts` (Cypress)
691
+ - Cover critical user flows: auth, main feature interactions, form submissions
692
+
693
+ ### Mobile (Expo / React Native)
694
+ - **E2E**: Detox
695
+ - Tests live in `e2e/` at project root
696
+ - Test files: `[feature].test.ts`
697
+ - Cover critical flows: login, main navigation, core feature interactions
698
+
699
+ ---
700
+
701
+ ## Critical Rules (Non-Negotiable)
702
+
703
+ 1. **`app/` is thin**: `page.tsx` only renders the feature page/screen. No logic, no state, no extra imports.
704
+ 2. **Pages are orchestrators**: Feature pages compose components — they are short. If the page is getting long, extract a component.
705
+ 3. **Components have one job**: If a component grows, split it. Name reflects its single purpose.
706
+ 4. **Always use `cn()`** for class merging — never template literals with conditional classes.
707
+ 5. **Forms always use `Controller`** from react-hook-form — never uncontrolled inputs.
708
+ 6. **API logic in use-cases**: Components and services never call `axios`/`api` directly — always through a use-case function.
709
+ 7. **Zod for all validation**: No manual validation — always define a schema and use `zodResolver`.
710
+ 8. **Type everything**: No `any`. All props, params, and return values must be typed.
711
+ 9. **Shared is truly shared**: Only put in `shared/` what is used by 2+ features.
712
+ 10. **Naming reflects role**: File name + suffix must make the file's purpose immediately obvious.