@zigrivers/scaffold 3.6.0 → 3.8.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 (115) hide show
  1. package/README.md +127 -12
  2. package/content/knowledge/backend/backend-api-design.md +103 -0
  3. package/content/knowledge/backend/backend-architecture.md +100 -0
  4. package/content/knowledge/backend/backend-async-patterns.md +101 -0
  5. package/content/knowledge/backend/backend-auth-patterns.md +100 -0
  6. package/content/knowledge/backend/backend-conventions.md +105 -0
  7. package/content/knowledge/backend/backend-data-modeling.md +102 -0
  8. package/content/knowledge/backend/backend-deployment.md +100 -0
  9. package/content/knowledge/backend/backend-dev-environment.md +102 -0
  10. package/content/knowledge/backend/backend-observability.md +102 -0
  11. package/content/knowledge/backend/backend-project-structure.md +100 -0
  12. package/content/knowledge/backend/backend-requirements.md +103 -0
  13. package/content/knowledge/backend/backend-security.md +104 -0
  14. package/content/knowledge/backend/backend-testing.md +101 -0
  15. package/content/knowledge/backend/backend-worker-patterns.md +100 -0
  16. package/content/knowledge/cli/cli-architecture.md +101 -0
  17. package/content/knowledge/cli/cli-conventions.md +117 -0
  18. package/content/knowledge/cli/cli-dev-environment.md +121 -0
  19. package/content/knowledge/cli/cli-distribution-patterns.md +106 -0
  20. package/content/knowledge/cli/cli-interactivity-patterns.md +116 -0
  21. package/content/knowledge/cli/cli-output-patterns.md +107 -0
  22. package/content/knowledge/cli/cli-project-structure.md +124 -0
  23. package/content/knowledge/cli/cli-requirements.md +101 -0
  24. package/content/knowledge/cli/cli-shell-integration.md +130 -0
  25. package/content/knowledge/cli/cli-testing.md +134 -0
  26. package/content/knowledge/library/library-api-design.md +306 -0
  27. package/content/knowledge/library/library-architecture.md +247 -0
  28. package/content/knowledge/library/library-bundling.md +244 -0
  29. package/content/knowledge/library/library-conventions.md +229 -0
  30. package/content/knowledge/library/library-dev-environment.md +220 -0
  31. package/content/knowledge/library/library-documentation.md +300 -0
  32. package/content/knowledge/library/library-project-structure.md +237 -0
  33. package/content/knowledge/library/library-requirements.md +173 -0
  34. package/content/knowledge/library/library-security.md +257 -0
  35. package/content/knowledge/library/library-testing.md +319 -0
  36. package/content/knowledge/library/library-type-definitions.md +284 -0
  37. package/content/knowledge/library/library-versioning.md +300 -0
  38. package/content/knowledge/mobile-app/mobile-app-architecture.md +283 -0
  39. package/content/knowledge/mobile-app/mobile-app-conventions.md +180 -0
  40. package/content/knowledge/mobile-app/mobile-app-deployment.md +298 -0
  41. package/content/knowledge/mobile-app/mobile-app-dev-environment.md +257 -0
  42. package/content/knowledge/mobile-app/mobile-app-distribution.md +264 -0
  43. package/content/knowledge/mobile-app/mobile-app-observability.md +317 -0
  44. package/content/knowledge/mobile-app/mobile-app-offline-patterns.md +311 -0
  45. package/content/knowledge/mobile-app/mobile-app-project-structure.md +245 -0
  46. package/content/knowledge/mobile-app/mobile-app-push-notifications.md +321 -0
  47. package/content/knowledge/mobile-app/mobile-app-requirements.md +147 -0
  48. package/content/knowledge/mobile-app/mobile-app-security.md +338 -0
  49. package/content/knowledge/mobile-app/mobile-app-testing.md +400 -0
  50. package/content/knowledge/web-app/web-app-api-patterns.md +224 -0
  51. package/content/knowledge/web-app/web-app-architecture.md +116 -0
  52. package/content/knowledge/web-app/web-app-auth-patterns.md +256 -0
  53. package/content/knowledge/web-app/web-app-conventions.md +121 -0
  54. package/content/knowledge/web-app/web-app-data-patterns.md +218 -0
  55. package/content/knowledge/web-app/web-app-deployment-workflow.md +143 -0
  56. package/content/knowledge/web-app/web-app-deployment.md +134 -0
  57. package/content/knowledge/web-app/web-app-design-system.md +158 -0
  58. package/content/knowledge/web-app/web-app-dev-environment.md +173 -0
  59. package/content/knowledge/web-app/web-app-observability.md +221 -0
  60. package/content/knowledge/web-app/web-app-project-structure.md +160 -0
  61. package/content/knowledge/web-app/web-app-rendering-strategies.md +133 -0
  62. package/content/knowledge/web-app/web-app-requirements.md +112 -0
  63. package/content/knowledge/web-app/web-app-security.md +193 -0
  64. package/content/knowledge/web-app/web-app-session-patterns.md +214 -0
  65. package/content/knowledge/web-app/web-app-testing.md +249 -0
  66. package/content/knowledge/web-app/web-app-ux-patterns.md +162 -0
  67. package/content/methodology/backend-overlay.yml +73 -0
  68. package/content/methodology/cli-overlay.yml +69 -0
  69. package/content/methodology/library-overlay.yml +67 -0
  70. package/content/methodology/mobile-app-overlay.yml +71 -0
  71. package/content/methodology/web-app-overlay.yml +79 -0
  72. package/dist/cli/commands/init.d.ts +21 -0
  73. package/dist/cli/commands/init.d.ts.map +1 -1
  74. package/dist/cli/commands/init.js +261 -13
  75. package/dist/cli/commands/init.js.map +1 -1
  76. package/dist/cli/commands/init.test.js +206 -0
  77. package/dist/cli/commands/init.test.js.map +1 -1
  78. package/dist/config/schema.d.ts +1392 -64
  79. package/dist/config/schema.d.ts.map +1 -1
  80. package/dist/config/schema.js +82 -5
  81. package/dist/config/schema.js.map +1 -1
  82. package/dist/config/schema.test.js +302 -1
  83. package/dist/config/schema.test.js.map +1 -1
  84. package/dist/core/assembly/overlay-loader.d.ts.map +1 -1
  85. package/dist/core/assembly/overlay-loader.js +2 -1
  86. package/dist/core/assembly/overlay-loader.js.map +1 -1
  87. package/dist/core/assembly/overlay-loader.test.js +56 -0
  88. package/dist/core/assembly/overlay-loader.test.js.map +1 -1
  89. package/dist/e2e/game-pipeline.test.js +1 -0
  90. package/dist/e2e/game-pipeline.test.js.map +1 -1
  91. package/dist/e2e/project-type-overlays.test.d.ts +16 -0
  92. package/dist/e2e/project-type-overlays.test.d.ts.map +1 -0
  93. package/dist/e2e/project-type-overlays.test.js +834 -0
  94. package/dist/e2e/project-type-overlays.test.js.map +1 -0
  95. package/dist/types/config.d.ts +19 -2
  96. package/dist/types/config.d.ts.map +1 -1
  97. package/dist/types/index.d.ts +0 -1
  98. package/dist/types/index.d.ts.map +1 -1
  99. package/dist/types/index.js +0 -1
  100. package/dist/types/index.js.map +1 -1
  101. package/dist/wizard/questions.d.ts +27 -1
  102. package/dist/wizard/questions.d.ts.map +1 -1
  103. package/dist/wizard/questions.js +142 -3
  104. package/dist/wizard/questions.js.map +1 -1
  105. package/dist/wizard/questions.test.js +206 -8
  106. package/dist/wizard/questions.test.js.map +1 -1
  107. package/dist/wizard/wizard.d.ts +21 -0
  108. package/dist/wizard/wizard.d.ts.map +1 -1
  109. package/dist/wizard/wizard.js +27 -1
  110. package/dist/wizard/wizard.js.map +1 -1
  111. package/package.json +1 -1
  112. package/dist/types/wizard.d.ts +0 -14
  113. package/dist/types/wizard.d.ts.map +0 -1
  114. package/dist/types/wizard.js +0 -2
  115. package/dist/types/wizard.js.map +0 -1
@@ -0,0 +1,400 @@
1
+ ---
2
+ name: mobile-app-testing
3
+ description: Unit tests, UI tests (XCTest/Espresso/Detox), snapshot tests, accessibility testing, and test architecture for iOS and Android
4
+ topics: [mobile-app, testing, xctest, espresso, detox, snapshot-testing, accessibility-testing, unit-tests]
5
+ ---
6
+
7
+ Mobile testing requires a multi-layer strategy: unit tests for business logic, integration tests for repository and network layers, UI tests for critical user flows, and snapshot tests for visual regression. The test pyramid applies — fast unit tests outnumber slow UI tests. Mobile UI tests are inherently fragile (timing, simulator state, animations) — structure them to minimize flakiness and run only the highest-value flows in CI.
8
+
9
+ ## Summary
10
+
11
+ iOS testing uses XCTest for unit and UI tests, with third-party additions (Quick/Nimble for BDD, snapshot-testing for visual regression). Android uses JUnit4/5 + Mockito/MockK for unit tests, Espresso for UI tests (in-process), and optional Detox for cross-platform end-to-end testing. Snapshot tests catch unintended UI changes automatically. Test architecture follows the same clean separation as production code — inject fakes, not mocks, for stable tests. Run unit tests on every commit; UI tests on PR merge.
12
+
13
+ ## Deep Guidance
14
+
15
+ ### iOS Unit Testing (XCTest)
16
+
17
+ **Test file structure**
18
+ ```swift
19
+ import XCTest
20
+ @testable import MyApp
21
+
22
+ final class UserProfileViewModelTests: XCTestCase {
23
+ var sut: UserProfileViewModel! // System Under Test
24
+ var mockRepository: MockUserRepository!
25
+
26
+ override func setUp() {
27
+ super.setUp()
28
+ mockRepository = MockUserRepository()
29
+ sut = UserProfileViewModel(repository: mockRepository)
30
+ }
31
+
32
+ override func tearDown() {
33
+ sut = nil
34
+ mockRepository = nil
35
+ super.tearDown()
36
+ }
37
+
38
+ func test_loadUser_success_setsUserOnState() async throws {
39
+ // Arrange
40
+ let expectedUser = User(id: "1", name: "Jane", email: "jane@example.com")
41
+ mockRepository.stubbedUser = expectedUser
42
+
43
+ // Act
44
+ await sut.loadUser(id: "1")
45
+
46
+ // Assert
47
+ XCTAssertEqual(sut.user, expectedUser)
48
+ XCTAssertFalse(sut.isLoading)
49
+ XCTAssertNil(sut.error)
50
+ }
51
+
52
+ func test_loadUser_failure_setsErrorOnState() async {
53
+ // Arrange
54
+ mockRepository.stubbedError = NetworkError.serverError(500)
55
+
56
+ // Act
57
+ await sut.loadUser(id: "1")
58
+
59
+ // Assert
60
+ XCTAssertNil(sut.user)
61
+ XCTAssertNotNil(sut.error)
62
+ }
63
+ }
64
+ ```
65
+
66
+ **Protocol-based fakes over Mocks**
67
+ ```swift
68
+ protocol UserRepository {
69
+ func fetchUser(id: String) async throws -> User
70
+ func updateUser(_ user: User) async throws
71
+ }
72
+
73
+ // Fake — implements the real behavior with in-memory data
74
+ final class FakeUserRepository: UserRepository {
75
+ var stubbedUser: User?
76
+ var stubbedError: Error?
77
+ var updatedUsers: [User] = []
78
+
79
+ func fetchUser(id: String) async throws -> User {
80
+ if let error = stubbedError { throw error }
81
+ return stubbedUser ?? User(id: id, name: "Test User", email: "test@example.com")
82
+ }
83
+
84
+ func updateUser(_ user: User) async throws {
85
+ if let error = stubbedError { throw error }
86
+ updatedUsers.append(user)
87
+ }
88
+ }
89
+ ```
90
+
91
+ Fakes are preferable to generated mocks (Cuckoo, Mockingbird) because they compile without code generation, work in Swift Previews, and test realistic behavior rather than exact call sequences.
92
+
93
+ **Testing async code**
94
+ ```swift
95
+ // Test async throws with async test methods (Swift concurrency)
96
+ func test_loadUser_callsRepository() async throws {
97
+ let result = try await sut.loadUser(id: "1")
98
+ XCTAssertEqual(result.id, "1")
99
+ }
100
+
101
+ // Test Combine publishers
102
+ func test_isLoading_trueWhileLoading() {
103
+ var loadingStates: [Bool] = []
104
+ let expectation = expectation(description: "Loading state changes")
105
+ expectation.expectedFulfillmentCount = 3 // false → true → false
106
+
107
+ let cancellable = sut.$isLoading.sink { loadingStates.append($0) }
108
+
109
+ Task { await sut.loadUser(id: "1") }
110
+
111
+ wait(for: [expectation], timeout: 2.0)
112
+ XCTAssertEqual(loadingStates, [false, true, false])
113
+ }
114
+
115
+ // Performance tests
116
+ func test_fetchAllUsers_performance() throws {
117
+ measure {
118
+ let _ = try! mockRepository.fetchAllUsers()
119
+ }
120
+ }
121
+ ```
122
+
123
+ ### Android Unit Testing (JUnit + MockK)
124
+
125
+ **ViewModel unit test**
126
+ ```kotlin
127
+ @OptIn(ExperimentalCoroutinesApi::class)
128
+ class UserProfileViewModelTest {
129
+
130
+ @get:Rule val mainDispatcherRule = MainDispatcherRule() // replaces Main dispatcher with test dispatcher
131
+
132
+ private val userRepository = mockk<UserRepository>()
133
+ private lateinit var viewModel: UserProfileViewModel
134
+
135
+ @BeforeEach
136
+ fun setUp() {
137
+ viewModel = UserProfileViewModel(userRepository)
138
+ }
139
+
140
+ @Test
141
+ fun `loadUser success sets user in state`() = runTest {
142
+ // Arrange
143
+ val expectedUser = User(id = "1", name = "Jane", email = "jane@example.com")
144
+ coEvery { userRepository.fetchUser("1") } returns Result.success(expectedUser)
145
+
146
+ // Act
147
+ viewModel.loadUser("1")
148
+ advanceUntilIdle()
149
+
150
+ // Assert
151
+ val state = viewModel.uiState.value
152
+ assertThat(state.user).isEqualTo(expectedUser)
153
+ assertThat(state.isLoading).isFalse()
154
+ assertThat(state.error).isNull()
155
+ }
156
+
157
+ @Test
158
+ fun `loadUser failure sets error in state`() = runTest {
159
+ coEvery { userRepository.fetchUser(any()) } returns Result.failure(IOException("Network error"))
160
+
161
+ viewModel.loadUser("1")
162
+ advanceUntilIdle()
163
+
164
+ assertThat(viewModel.uiState.value.error).isNotNull()
165
+ }
166
+ }
167
+ ```
168
+
169
+ **MainDispatcherRule (required for ViewModel tests)**
170
+ ```kotlin
171
+ class MainDispatcherRule(
172
+ private val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
173
+ ) : TestWatcher() {
174
+ override fun starting(description: Description) {
175
+ Dispatchers.setMain(dispatcher)
176
+ }
177
+ override fun finished(description: Description) {
178
+ Dispatchers.resetMain()
179
+ dispatcher.cleanupTestCoroutines()
180
+ }
181
+ }
182
+ ```
183
+
184
+ **Repository integration test (with in-memory Room)**
185
+ ```kotlin
186
+ @RunWith(AndroidJUnit4::class)
187
+ class UserRepositoryTest {
188
+ private lateinit var database: AppDatabase
189
+ private lateinit var userDao: UserDao
190
+
191
+ @Before
192
+ fun setUp() {
193
+ database = Room.inMemoryDatabaseBuilder(
194
+ ApplicationProvider.getApplicationContext(),
195
+ AppDatabase::class.java
196
+ ).allowMainThreadQueries().build()
197
+ userDao = database.userDao()
198
+ }
199
+
200
+ @After
201
+ fun tearDown() {
202
+ database.close()
203
+ }
204
+
205
+ @Test
206
+ fun insertAndRetrieveUser() = runTest {
207
+ val user = UserEntity(id = "1", name = "Jane", email = "jane@example.com", updatedAt = 0L)
208
+ userDao.upsert(user)
209
+ val retrieved = userDao.observeUsers().first()
210
+ assertThat(retrieved).contains(user)
211
+ }
212
+ }
213
+ ```
214
+
215
+ ### UI Testing
216
+
217
+ **iOS XCUITest**
218
+ ```swift
219
+ final class LoginUITests: XCTestCase {
220
+ var app: XCUIApplication!
221
+
222
+ override func setUpWithError() throws {
223
+ continueAfterFailure = false
224
+ app = XCUIApplication()
225
+ app.launchArguments = ["--uitesting", "--reset-state"]
226
+ app.launch()
227
+ }
228
+
229
+ func test_login_validCredentials_navigatesToHome() throws {
230
+ let emailField = app.textFields["login_email_field"]
231
+ XCTAssertTrue(emailField.waitForExistence(timeout: 5))
232
+ emailField.tap()
233
+ emailField.typeText("test@example.com")
234
+
235
+ let passwordField = app.secureTextFields["login_password_field"]
236
+ passwordField.tap()
237
+ passwordField.typeText("password123")
238
+
239
+ app.buttons["login_submit_button"].tap()
240
+
241
+ let homeTitle = app.staticTexts["home_screen_title"]
242
+ XCTAssertTrue(homeTitle.waitForExistence(timeout: 10))
243
+ }
244
+ }
245
+ ```
246
+
247
+ Accessibility identifiers:
248
+ ```swift
249
+ // Set in SwiftUI
250
+ Button("Submit") { submitAction() }
251
+ .accessibilityIdentifier("login_submit_button")
252
+
253
+ // Set in UIKit
254
+ loginButton.accessibilityIdentifier = "login_submit_button"
255
+ ```
256
+
257
+ Use `accessibilityIdentifier` for test targeting — never use display text or position-based queries which break on localization or layout changes.
258
+
259
+ **Android Espresso**
260
+ ```kotlin
261
+ @RunWith(AndroidJUnit4::class)
262
+ class LoginInstrumentedTest {
263
+
264
+ @get:Rule val activityRule = ActivityScenarioRule(LoginActivity::class.java)
265
+
266
+ @Test
267
+ fun login_validCredentials_navigatesToHome() {
268
+ onView(withId(R.id.email_field))
269
+ .perform(typeText("test@example.com"), closeSoftKeyboard())
270
+ onView(withId(R.id.password_field))
271
+ .perform(typeText("password123"), closeSoftKeyboard())
272
+ onView(withId(R.id.submit_button)).perform(click())
273
+
274
+ onView(withId(R.id.home_screen_title))
275
+ .check(matches(isDisplayed()))
276
+ }
277
+ }
278
+ ```
279
+
280
+ **Compose UI testing**
281
+ ```kotlin
282
+ @RunWith(AndroidJUnit4::class)
283
+ class UserProfileScreenTest {
284
+
285
+ @get:Rule val composeTestRule = createComposeRule()
286
+
287
+ @Test
288
+ fun userCard_displaysName() {
289
+ val user = User(id = "1", name = "Jane Smith", email = "jane@example.com")
290
+ composeTestRule.setContent {
291
+ MyAppTheme { UserCard(user = user) }
292
+ }
293
+
294
+ composeTestRule.onNodeWithText("Jane Smith").assertIsDisplayed()
295
+ composeTestRule.onNodeWithContentDescription("User avatar").assertExists()
296
+ }
297
+ }
298
+ ```
299
+
300
+ ### Snapshot Testing
301
+
302
+ **iOS: swift-snapshot-testing (Point-Free)**
303
+ ```swift
304
+ import SnapshotTesting
305
+
306
+ final class UserCardSnapshotTests: XCTestCase {
307
+ func test_userCard_defaultState() {
308
+ let view = UserCardView(user: .fixture)
309
+ assertSnapshot(of: view, as: .image(on: .iPhone13Pro))
310
+ }
311
+
312
+ func test_userCard_loadingState() {
313
+ let view = UserCardView(isLoading: true)
314
+ assertSnapshot(of: view, as: .image(on: .iPhone13Pro))
315
+ }
316
+
317
+ func test_userCard_darkMode() {
318
+ let view = UserCardView(user: .fixture)
319
+ assertSnapshot(of: view, as: .image(on: .iPhone13Pro, traits: .init(userInterfaceStyle: .dark)))
320
+ }
321
+ }
322
+ ```
323
+
324
+ Run snapshots in `record` mode once to generate reference images, then in normal mode to detect regressions. Commit reference images to git. Update references only for intentional UI changes.
325
+
326
+ **Android: Paparazzi (Cashapp)**
327
+ ```kotlin
328
+ @RunWith(AndroidJUnit4::class)
329
+ class UserCardSnapshotTest {
330
+ @get:Rule val paparazzi = Paparazzi(deviceConfig = DeviceConfig.PIXEL_6)
331
+
332
+ @Test
333
+ fun userCard_defaultState() {
334
+ paparazzi.snapshot {
335
+ MyAppTheme {
336
+ UserCard(user = previewUser)
337
+ }
338
+ }
339
+ }
340
+ }
341
+ ```
342
+
343
+ Paparazzi runs on the JVM without a device or emulator — fast, CI-friendly.
344
+
345
+ ### Accessibility Testing
346
+
347
+ **iOS: Accessibility Inspector + XCTest**
348
+ ```swift
349
+ func test_loginButton_hasAccessibilityLabel() {
350
+ let button = app.buttons["login_submit_button"]
351
+ XCTAssertTrue(button.exists)
352
+ XCTAssertFalse(button.label.isEmpty) // has accessibility label
353
+ }
354
+ ```
355
+
356
+ Automated accessibility audit:
357
+ ```swift
358
+ func test_homeScreen_passesAccessibilityAudit() throws {
359
+ // iOS 17+: built-in accessibility audit
360
+ try app.performAccessibilityAudit()
361
+ }
362
+ ```
363
+
364
+ `performAccessibilityAudit()` checks: missing labels, insufficient contrast, small touch targets, and other WCAG violations automatically.
365
+
366
+ **Android: Accessibility Test Framework**
367
+ ```kotlin
368
+ @RunWith(AndroidJUnit4::class)
369
+ class AccessibilityTest {
370
+ @get:Rule val rule = AccessibilityChecksRule() // auto-runs checks on every view interaction
371
+
372
+ @Test
373
+ fun loginScreen_allElementsAccessible() {
374
+ onView(withId(R.id.submit_button)).perform(click())
375
+ // AccessibilityChecksRule automatically fails on accessibility issues
376
+ }
377
+ }
378
+ ```
379
+
380
+ Compose accessibility testing:
381
+ ```kotlin
382
+ composeTestRule.onNodeWithText("Submit")
383
+ .assertHasClickAction()
384
+ .assertContentDescriptionEquals("Submit order")
385
+ .assertIsEnabled()
386
+ ```
387
+
388
+ ### CI Test Strategy
389
+
390
+ **What runs when**
391
+ - Every commit: unit tests (fast, < 60 seconds)
392
+ - Every PR: unit tests + instrumented tests on emulator (< 10 minutes)
393
+ - Every merge to main: full suite including UI tests and snapshot tests (< 30 minutes)
394
+ - Nightly: device farm run across full device matrix
395
+
396
+ **Handling flaky tests**
397
+ - Retry mechanism in CI: rerun failed tests up to 2 times before marking as failed
398
+ - iOS: `--retry-tests-on-failure` flag in `xcodebuild test`
399
+ - Android: `android { testOptions { unitTests { isReturnDefaultValues = true } } }`
400
+ - Track flaky tests in a flakiness registry — tests flagged as flaky do not block merges but get prioritized for fixing
@@ -0,0 +1,224 @@
1
+ ---
2
+ name: web-app-api-patterns
3
+ description: REST API design for web clients, GraphQL client patterns, error handling strategies, request deduplication, auth injection, and CORS
4
+ topics: [web-app, api, rest, graphql, cors, error-handling, auth]
5
+ ---
6
+
7
+ The API layer is the seam between frontend and backend. Poor design here manifests as waterfall requests that serialize page loads, inconsistent error shapes that require fragile client-side guessing, auth token handling bugs that cause random 401 errors, and CORS misconfigurations that block legitimate requests. A well-designed API client is boring: it handles auth transparently, errors consistently, and requests efficiently — so product engineers can focus on features rather than network plumbing.
8
+
9
+ ## Summary
10
+
11
+ ### REST API Design for Web Clients
12
+
13
+ REST conventions for web clients go beyond HTTP verb selection:
14
+
15
+ **Resource naming:** `/users/{id}/posts` not `/getUserPosts`. Nouns, not verbs. Plural collections.
16
+
17
+ **Consistent response envelope:** Every response should have a predictable shape. Either always return the resource directly (`200 OK` with the object) or always wrap it (`{ data: {...}, meta: {...} }`) — never mix.
18
+
19
+ **Status codes must be semantically correct:**
20
+ - `200 OK` — successful read
21
+ - `201 Created` — successful create (include `Location` header with new resource URL)
22
+ - `204 No Content` — successful delete or update with no response body
23
+ - `400 Bad Request` — validation failure (include field-level errors)
24
+ - `401 Unauthorized` — not authenticated
25
+ - `403 Forbidden` — authenticated but not authorized
26
+ - `404 Not Found` — resource does not exist
27
+ - `409 Conflict` — state conflict (duplicate resource, version mismatch)
28
+ - `422 Unprocessable Entity` — business rule violation
29
+ - `429 Too Many Requests` — rate limited (include `Retry-After` header)
30
+
31
+ **Error response shape (standardize on RFC 7807 Problem Details):**
32
+ ```json
33
+ {
34
+ "type": "https://api.example.com/errors/validation",
35
+ "title": "Validation Failed",
36
+ "status": 400,
37
+ "detail": "2 fields failed validation",
38
+ "errors": [
39
+ { "field": "email", "message": "Invalid email format" },
40
+ { "field": "username", "message": "Username already taken" }
41
+ ]
42
+ }
43
+ ```
44
+
45
+ ### GraphQL Client Patterns
46
+
47
+ GraphQL shifts the API design from server-defined endpoints to client-defined data requirements:
48
+
49
+ **Fragments for co-location:** Define data requirements alongside the component that uses the data. This prevents over-fetching and makes it obvious when a component's data requirements change.
50
+
51
+ **Normalized caching:** Apollo Client and urql maintain a normalized cache where each entity is stored once (by `__typename + id`) and automatically updated across all queries that reference it. Mutations that return the updated entity automatically update all queries that include that entity.
52
+
53
+ **Fragment colocation pattern:** Each component defines its own fragment; the parent query composes them. This is the Relay-inspired pattern that scales to large teams.
54
+
55
+ ### Error Handling: Toast vs Inline
56
+
57
+ Two display patterns for API errors, each appropriate in different contexts:
58
+
59
+ **Toast notifications:** Background mutations, non-blocking operations, and errors where the user's current context is unchanged. "Failed to save changes — try again." The user doesn't lose their work.
60
+
61
+ **Inline errors:** Form submissions, operations where the error requires user action to resolve. Show the error adjacent to the field or action that caused it. A `400` validation error must show which fields failed.
62
+
63
+ **Never show raw API errors to users.** Map error codes to human-readable messages on the client. Log the technical detail to your error tracker.
64
+
65
+ ### Request Deduplication
66
+
67
+ Multiple components mounting simultaneously and each calling the same endpoint produces redundant parallel requests. Deduplication ensures the second identical in-flight request waits for the first's result rather than issuing a new request.
68
+
69
+ React Query deduplicates automatically — all components with the same `queryKey` share one in-flight request. For custom fetch layers, implement deduplication with a request-in-flight map.
70
+
71
+ ## Deep Guidance
72
+
73
+ ### API Client with Auth Injection and Token Refresh
74
+
75
+ ```typescript
76
+ // api-client.ts — Centralized API client with automatic token refresh
77
+ import ky from 'ky'; // Or axios, or native fetch
78
+
79
+ let isRefreshing = false;
80
+ let refreshQueue: Array<(token: string) => void> = [];
81
+
82
+ export const apiClient = ky.create({
83
+ prefixUrl: process.env.NEXT_PUBLIC_API_URL,
84
+ hooks: {
85
+ beforeRequest: [
86
+ async (request) => {
87
+ const token = getAccessToken(); // From memory, not localStorage
88
+ if (token) {
89
+ request.headers.set('Authorization', `Bearer ${token}`);
90
+ }
91
+ },
92
+ ],
93
+ afterResponse: [
94
+ async (request, options, response) => {
95
+ if (response.status !== 401) return response;
96
+
97
+ // Token expired — refresh and retry
98
+ if (isRefreshing) {
99
+ // Queue this request until refresh completes
100
+ return new Promise((resolve) => {
101
+ refreshQueue.push((newToken) => {
102
+ request.headers.set('Authorization', `Bearer ${newToken}`);
103
+ resolve(ky(request));
104
+ });
105
+ });
106
+ }
107
+
108
+ isRefreshing = true;
109
+ try {
110
+ const newToken = await refreshAccessToken();
111
+ setAccessToken(newToken);
112
+
113
+ // Replay all queued requests with new token
114
+ refreshQueue.forEach(cb => cb(newToken));
115
+ refreshQueue = [];
116
+
117
+ // Retry the original request
118
+ request.headers.set('Authorization', `Bearer ${newToken}`);
119
+ return ky(request);
120
+ } catch (refreshError) {
121
+ // Refresh failed — redirect to login
122
+ clearSession();
123
+ window.location.href = '/login';
124
+ throw refreshError;
125
+ } finally {
126
+ isRefreshing = false;
127
+ }
128
+ },
129
+ ],
130
+ },
131
+ });
132
+ ```
133
+
134
+ The token refresh queue pattern prevents multiple simultaneous 401s from triggering multiple refresh requests. Only the first request initiates the refresh; subsequent requests queue and replay once the token is available.
135
+
136
+ ### CORS Configuration
137
+
138
+ CORS is a browser-enforced security mechanism, not an API security mechanism. Servers must explicitly allow cross-origin requests from trusted origins.
139
+
140
+ ```typescript
141
+ // Express CORS configuration
142
+ import cors from 'cors';
143
+
144
+ const allowedOrigins = [
145
+ 'https://app.example.com',
146
+ 'https://www.example.com',
147
+ // Development origins — only in non-production
148
+ ...(process.env.NODE_ENV !== 'production' ? ['http://localhost:3000'] : []),
149
+ ];
150
+
151
+ app.use(cors({
152
+ origin: (origin, callback) => {
153
+ // Allow requests with no origin (server-to-server, curl)
154
+ if (!origin) return callback(null, true);
155
+ if (allowedOrigins.includes(origin)) return callback(null, true);
156
+ callback(new Error(`Origin ${origin} not allowed by CORS policy`));
157
+ },
158
+ credentials: true, // Required when client sends cookies
159
+ methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
160
+ allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'],
161
+ exposedHeaders: ['X-Request-ID', 'X-Rate-Limit-Remaining'],
162
+ maxAge: 86400, // Cache preflight for 24 hours (reduces OPTIONS requests)
163
+ }));
164
+ ```
165
+
166
+ **CORS mistakes to avoid:**
167
+ - `origin: '*'` with `credentials: true` is invalid and rejected by browsers
168
+ - Wildcard subdomain matching (`*.example.com`) requires explicit pattern matching, not a string
169
+ - Missing `maxAge` causes an OPTIONS preflight on every non-simple cross-origin request
170
+
171
+ ### GraphQL Fragment Colocation
172
+
173
+ ```typescript
174
+ // UserCard.tsx — Component defines its own data requirements
175
+ const USER_CARD_FRAGMENT = gql`
176
+ fragment UserCardFields on User {
177
+ id
178
+ displayName
179
+ avatarUrl
180
+ followerCount
181
+ }
182
+ `;
183
+
184
+ function UserCard({ user }: { user: UserCardFields }) {
185
+ return (/* render using user.displayName, user.avatarUrl, etc. */);
186
+ }
187
+
188
+ UserCard.fragments = { user: USER_CARD_FRAGMENT };
189
+
190
+ // ProfilePage.tsx — Composes fragments, doesn't know what UserCard needs
191
+ const PROFILE_PAGE_QUERY = gql`
192
+ query ProfilePage($userId: ID!) {
193
+ user(id: $userId) {
194
+ ...UserCardFields
195
+ email # Only ProfilePage needs this
196
+ createdAt
197
+ }
198
+ }
199
+ ${UserCard.fragments.user}
200
+ `;
201
+ ```
202
+
203
+ When `UserCard` needs a new field, the change is isolated to the fragment and its parent query is automatically updated. No need to modify multiple components or add fields "just in case."
204
+
205
+ ### API Error Handling Pattern
206
+
207
+ ```typescript
208
+ // Typed error handling — map API errors to user-facing messages
209
+ const API_ERROR_MESSAGES: Record<string, string> = {
210
+ 'validation/email-taken': 'That email address is already registered.',
211
+ 'auth/session-expired': 'Your session has expired. Please sign in again.',
212
+ 'rate-limit/exceeded': 'Too many attempts. Please wait a moment and try again.',
213
+ };
214
+
215
+ function getErrorMessage(error: ApiError): string {
216
+ if (error.type && API_ERROR_MESSAGES[error.type]) {
217
+ return API_ERROR_MESSAGES[error.type];
218
+ }
219
+ // Safe fallback — never expose raw server errors
220
+ return 'Something went wrong. Please try again.';
221
+ }
222
+ ```
223
+
224
+ Maintain error message strings in one place. Never construct user-facing messages from raw `error.message` strings returned by the server.