@schafevormfenster/rest-commons 0.1.1

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 (264) hide show
  1. package/CONTRIBUTING.md +1190 -0
  2. package/README.md +275 -0
  3. package/bin/setup.js +10 -0
  4. package/dist/api-schemas/error.schema.d.ts +20 -0
  5. package/dist/api-schemas/error.schema.d.ts.map +1 -0
  6. package/dist/api-schemas/error.schema.js +17 -0
  7. package/dist/api-schemas/health.schema.d.ts +497 -0
  8. package/dist/api-schemas/health.schema.d.ts.map +1 -0
  9. package/dist/api-schemas/health.schema.js +33 -0
  10. package/dist/api-schemas/okay.schema.d.ts +13 -0
  11. package/dist/api-schemas/okay.schema.d.ts.map +1 -0
  12. package/dist/api-schemas/okay.schema.js +5 -0
  13. package/dist/api-schemas/paginated-results.schema.d.ts +59 -0
  14. package/dist/api-schemas/paginated-results.schema.d.ts.map +1 -0
  15. package/dist/api-schemas/paginated-results.schema.js +10 -0
  16. package/dist/api-schemas/partial-results.schema.d.ts +30 -0
  17. package/dist/api-schemas/partial-results.schema.d.ts.map +1 -0
  18. package/dist/api-schemas/partial-results.schema.js +10 -0
  19. package/dist/api-schemas/result.schema.d.ts +17 -0
  20. package/dist/api-schemas/result.schema.d.ts.map +1 -0
  21. package/dist/api-schemas/result.schema.js +5 -0
  22. package/dist/api-schemas/results.schema.d.ts +21 -0
  23. package/dist/api-schemas/results.schema.d.ts.map +1 -0
  24. package/dist/api-schemas/results.schema.js +5 -0
  25. package/dist/helpers/correlation/get-correlation-id.d.ts +7 -0
  26. package/dist/helpers/correlation/get-correlation-id.d.ts.map +1 -0
  27. package/dist/helpers/correlation/get-correlation-id.js +16 -0
  28. package/dist/helpers/correlation/get-header.d.ts +7 -0
  29. package/dist/helpers/correlation/get-header.d.ts.map +1 -0
  30. package/dist/helpers/correlation/get-header.js +11 -0
  31. package/dist/helpers/detect-mime-type.d.ts +11 -0
  32. package/dist/helpers/detect-mime-type.d.ts.map +1 -0
  33. package/dist/helpers/detect-mime-type.js +40 -0
  34. package/dist/helpers/detect-suspicious-patterns.d.ts +8 -0
  35. package/dist/helpers/detect-suspicious-patterns.d.ts.map +1 -0
  36. package/dist/helpers/detect-suspicious-patterns.js +55 -0
  37. package/dist/helpers/eventify-constants.types.d.ts +32 -0
  38. package/dist/helpers/eventify-constants.types.d.ts.map +1 -0
  39. package/dist/helpers/eventify-constants.types.js +40 -0
  40. package/dist/helpers/hash-binary.d.ts +21 -0
  41. package/dist/helpers/hash-binary.d.ts.map +1 -0
  42. package/dist/helpers/hash-binary.js +28 -0
  43. package/dist/helpers/mime-types/detect-image-mime-type.d.ts +5 -0
  44. package/dist/helpers/mime-types/detect-image-mime-type.d.ts.map +1 -0
  45. package/dist/helpers/mime-types/detect-image-mime-type.js +41 -0
  46. package/dist/helpers/mime-types/detect-ole-mime-type.d.ts +6 -0
  47. package/dist/helpers/mime-types/detect-ole-mime-type.d.ts.map +1 -0
  48. package/dist/helpers/mime-types/detect-ole-mime-type.js +34 -0
  49. package/dist/helpers/mime-types/detect-pdf-mime-type.d.ts +5 -0
  50. package/dist/helpers/mime-types/detect-pdf-mime-type.d.ts.map +1 -0
  51. package/dist/helpers/mime-types/detect-pdf-mime-type.js +13 -0
  52. package/dist/helpers/mime-types/detect-zip-mime-type.d.ts +6 -0
  53. package/dist/helpers/mime-types/detect-zip-mime-type.d.ts.map +1 -0
  54. package/dist/helpers/mime-types/detect-zip-mime-type.js +23 -0
  55. package/dist/helpers/parameter-validation.d.ts +6 -0
  56. package/dist/helpers/parameter-validation.d.ts.map +1 -0
  57. package/dist/helpers/parameter-validation.js +19 -0
  58. package/dist/helpers/parameter-validation.types.d.ts +16 -0
  59. package/dist/helpers/parameter-validation.types.d.ts.map +1 -0
  60. package/dist/helpers/parameter-validation.types.js +38 -0
  61. package/dist/helpers/response-headers/build-api-unauthorized-headers.d.ts +6 -0
  62. package/dist/helpers/response-headers/build-api-unauthorized-headers.d.ts.map +1 -0
  63. package/dist/helpers/response-headers/build-api-unauthorized-headers.js +23 -0
  64. package/dist/helpers/response-headers/environment.types.d.ts +2 -0
  65. package/dist/helpers/response-headers/environment.types.d.ts.map +1 -0
  66. package/dist/helpers/response-headers/environment.types.js +1 -0
  67. package/dist/helpers/response-headers/resolve-environment.d.ts +8 -0
  68. package/dist/helpers/response-headers/resolve-environment.d.ts.map +1 -0
  69. package/dist/helpers/response-headers/resolve-environment.js +18 -0
  70. package/dist/helpers/slugify.d.ts +15 -0
  71. package/dist/helpers/slugify.d.ts.map +1 -0
  72. package/dist/helpers/slugify.js +32 -0
  73. package/dist/index.d.ts +36 -0
  74. package/dist/index.d.ts.map +1 -0
  75. package/dist/index.js +41 -0
  76. package/dist/normalization/normalize-list.d.ts +11 -0
  77. package/dist/normalization/normalize-list.d.ts.map +1 -0
  78. package/dist/normalization/normalize-list.js +19 -0
  79. package/dist/normalization/normalize-location.d.ts +16 -0
  80. package/dist/normalization/normalize-location.d.ts.map +1 -0
  81. package/dist/normalization/normalize-location.js +26 -0
  82. package/dist/primitives/coordinate-precision.d.ts +10 -0
  83. package/dist/primitives/coordinate-precision.d.ts.map +1 -0
  84. package/dist/primitives/coordinate-precision.js +27 -0
  85. package/dist/primitives/geo-point.schema.d.ts +8 -0
  86. package/dist/primitives/geo-point.schema.d.ts.map +1 -0
  87. package/dist/primitives/geo-point.schema.js +10 -0
  88. package/dist/primitives/geoname-id.schema.d.ts +8 -0
  89. package/dist/primitives/geoname-id.schema.d.ts.map +1 -0
  90. package/dist/primitives/geoname-id.schema.js +9 -0
  91. package/dist/primitives/international-zip.schema.d.ts +76 -0
  92. package/dist/primitives/international-zip.schema.d.ts.map +1 -0
  93. package/dist/primitives/international-zip.schema.js +81 -0
  94. package/dist/primitives/latitude.schema.d.ts +9 -0
  95. package/dist/primitives/latitude.schema.d.ts.map +1 -0
  96. package/dist/primitives/latitude.schema.js +13 -0
  97. package/dist/primitives/location.schema.d.ts +8 -0
  98. package/dist/primitives/location.schema.d.ts.map +1 -0
  99. package/dist/primitives/location.schema.js +15 -0
  100. package/dist/primitives/longitude.schema.d.ts +9 -0
  101. package/dist/primitives/longitude.schema.d.ts.map +1 -0
  102. package/dist/primitives/longitude.schema.js +13 -0
  103. package/dist/primitives/numeric-id.schema.d.ts +8 -0
  104. package/dist/primitives/numeric-id.schema.d.ts.map +1 -0
  105. package/dist/primitives/numeric-id.schema.js +10 -0
  106. package/dist/primitives/slug.schema.d.ts +17 -0
  107. package/dist/primitives/slug.schema.d.ts.map +1 -0
  108. package/dist/primitives/slug.schema.js +30 -0
  109. package/dist/primitives/uuid.schema.d.ts +8 -0
  110. package/dist/primitives/uuid.schema.d.ts.map +1 -0
  111. package/dist/primitives/uuid.schema.js +9 -0
  112. package/dist/primitives/wikidata-id.schema.d.ts +9 -0
  113. package/dist/primitives/wikidata-id.schema.d.ts.map +1 -0
  114. package/dist/primitives/wikidata-id.schema.js +10 -0
  115. package/dist/time/boundary-enforcement.d.ts +11 -0
  116. package/dist/time/boundary-enforcement.d.ts.map +1 -0
  117. package/dist/time/boundary-enforcement.js +43 -0
  118. package/dist/time/bounded-time.schema.d.ts +31 -0
  119. package/dist/time/bounded-time.schema.d.ts.map +1 -0
  120. package/dist/time/bounded-time.schema.js +77 -0
  121. package/dist/time/flexible-time-parser.d.ts +12 -0
  122. package/dist/time/flexible-time-parser.d.ts.map +1 -0
  123. package/dist/time/flexible-time-parser.js +94 -0
  124. package/dist/time/flexible-time.schema.d.ts +31 -0
  125. package/dist/time/flexible-time.schema.d.ts.map +1 -0
  126. package/dist/time/flexible-time.schema.js +31 -0
  127. package/dist/time/get-week-end.d.ts +10 -0
  128. package/dist/time/get-week-end.d.ts.map +1 -0
  129. package/dist/time/get-week-end.js +25 -0
  130. package/dist/time/get-week-start.d.ts +10 -0
  131. package/dist/time/get-week-start.d.ts.map +1 -0
  132. package/dist/time/get-week-start.js +25 -0
  133. package/dist/time/is-relative-time.d.ts +8 -0
  134. package/dist/time/is-relative-time.d.ts.map +1 -0
  135. package/dist/time/is-relative-time.js +9 -0
  136. package/dist/time/iso8601.schema.d.ts +14 -0
  137. package/dist/time/iso8601.schema.d.ts.map +1 -0
  138. package/dist/time/iso8601.schema.js +17 -0
  139. package/dist/time/iso8601.types.d.ts +6 -0
  140. package/dist/time/iso8601.types.d.ts.map +1 -0
  141. package/dist/time/iso8601.types.js +11 -0
  142. package/dist/time/parse-relative-time.d.ts +9 -0
  143. package/dist/time/parse-relative-time.d.ts.map +1 -0
  144. package/dist/time/parse-relative-time.js +36 -0
  145. package/dist/time/relative-time.schema.d.ts +23 -0
  146. package/dist/time/relative-time.schema.d.ts.map +1 -0
  147. package/dist/time/relative-time.schema.js +25 -0
  148. package/dist/time/since-parameter.schema.d.ts +8 -0
  149. package/dist/time/since-parameter.schema.d.ts.map +1 -0
  150. package/dist/time/since-parameter.schema.js +56 -0
  151. package/dist/time/time-helpers.d.ts +19 -0
  152. package/dist/time/time-helpers.d.ts.map +1 -0
  153. package/dist/time/time-helpers.js +56 -0
  154. package/dist/time/time-schemas.d.ts +20 -0
  155. package/dist/time/time-schemas.d.ts.map +1 -0
  156. package/dist/time/time-schemas.js +25 -0
  157. package/dist/time/timezone.types.d.ts +17 -0
  158. package/dist/time/timezone.types.d.ts.map +1 -0
  159. package/dist/time/timezone.types.js +15 -0
  160. package/dist/validation/zod-error-handler.d.ts +3 -0
  161. package/dist/validation/zod-error-handler.d.ts.map +1 -0
  162. package/dist/validation/zod-error-handler.js +189 -0
  163. package/dist/validation/zod-utils.d.ts +9 -0
  164. package/dist/validation/zod-utils.d.ts.map +1 -0
  165. package/dist/validation/zod-utils.js +23 -0
  166. package/eslint.config.mjs +16 -0
  167. package/package.json +44 -0
  168. package/src/api-schemas/error.schema.test.ts +27 -0
  169. package/src/api-schemas/error.schema.ts +23 -0
  170. package/src/api-schemas/health.schema.test.ts +104 -0
  171. package/src/api-schemas/health.schema.ts +63 -0
  172. package/src/api-schemas/okay.schema.test.ts +15 -0
  173. package/src/api-schemas/okay.schema.ts +8 -0
  174. package/src/api-schemas/paginated-results.schema.ts +17 -0
  175. package/src/api-schemas/partial-results.schema.ts +13 -0
  176. package/src/api-schemas/result.schema.test.ts +19 -0
  177. package/src/api-schemas/result.schema.ts +9 -0
  178. package/src/api-schemas/results.schema.test.ts +15 -0
  179. package/src/api-schemas/results.schema.ts +9 -0
  180. package/src/helpers/correlation/get-correlation-id.test.ts +126 -0
  181. package/src/helpers/correlation/get-correlation-id.ts +22 -0
  182. package/src/helpers/correlation/get-header.test.ts +179 -0
  183. package/src/helpers/correlation/get-header.ts +21 -0
  184. package/src/helpers/detect-mime-type.test.ts +100 -0
  185. package/src/helpers/detect-mime-type.ts +46 -0
  186. package/src/helpers/detect-suspicious-patterns.test.ts +45 -0
  187. package/src/helpers/detect-suspicious-patterns.ts +57 -0
  188. package/src/helpers/eventify-constants.test.ts +52 -0
  189. package/src/helpers/eventify-constants.types.test.ts +52 -0
  190. package/src/helpers/eventify-constants.types.ts +51 -0
  191. package/src/helpers/hash-binary.test.ts +60 -0
  192. package/src/helpers/hash-binary.ts +30 -0
  193. package/src/helpers/mime-types/detect-image-mime-type.test.ts +73 -0
  194. package/src/helpers/mime-types/detect-image-mime-type.ts +50 -0
  195. package/src/helpers/mime-types/detect-ole-mime-type.test.ts +86 -0
  196. package/src/helpers/mime-types/detect-ole-mime-type.ts +44 -0
  197. package/src/helpers/mime-types/detect-pdf-mime-type.test.ts +39 -0
  198. package/src/helpers/mime-types/detect-pdf-mime-type.ts +15 -0
  199. package/src/helpers/mime-types/detect-zip-mime-type.test.ts +88 -0
  200. package/src/helpers/mime-types/detect-zip-mime-type.ts +28 -0
  201. package/src/helpers/parameter-validation.test.ts +35 -0
  202. package/src/helpers/parameter-validation.ts +32 -0
  203. package/src/helpers/process-eventify-request.ts +146 -0
  204. package/src/helpers/response-headers/build-api-unauthorized-headers.ts +30 -0
  205. package/src/helpers/response-headers/environment.types.ts +1 -0
  206. package/src/helpers/response-headers/resolve-environment.ts +17 -0
  207. package/src/helpers/slugify.test.ts +77 -0
  208. package/src/helpers/slugify.ts +34 -0
  209. package/src/index.ts +46 -0
  210. package/src/normalization/normalize-list.test.ts +43 -0
  211. package/src/normalization/normalize-list.ts +21 -0
  212. package/src/normalization/normalize-location.test.ts +91 -0
  213. package/src/normalization/normalize-location.ts +29 -0
  214. package/src/primitives/coordinate-precision.test.ts +46 -0
  215. package/src/primitives/coordinate-precision.ts +30 -0
  216. package/src/primitives/geo-point.schema.test.ts +70 -0
  217. package/src/primitives/geo-point.schema.ts +14 -0
  218. package/src/primitives/geoname-id.schema.test.ts +60 -0
  219. package/src/primitives/geoname-id.schema.ts +12 -0
  220. package/src/primitives/international-zip.schema.test.ts +212 -0
  221. package/src/primitives/international-zip.schema.ts +103 -0
  222. package/src/primitives/latitude.schema.test.ts +77 -0
  223. package/src/primitives/latitude.schema.ts +20 -0
  224. package/src/primitives/location.schema.test.ts +21 -0
  225. package/src/primitives/location.schema.ts +22 -0
  226. package/src/primitives/longitude.schema.test.ts +77 -0
  227. package/src/primitives/longitude.schema.ts +20 -0
  228. package/src/primitives/numeric-id.schema.test.ts +32 -0
  229. package/src/primitives/numeric-id.schema.ts +13 -0
  230. package/src/primitives/slug.schema.test.ts +101 -0
  231. package/src/primitives/slug.schema.ts +41 -0
  232. package/src/primitives/uuid.schema.test.ts +45 -0
  233. package/src/primitives/uuid.schema.ts +12 -0
  234. package/src/primitives/wikidata-id.schema.test.ts +51 -0
  235. package/src/primitives/wikidata-id.schema.ts +16 -0
  236. package/src/time/README.md +220 -0
  237. package/src/time/boundary-enforcement.test.ts +130 -0
  238. package/src/time/boundary-enforcement.ts +59 -0
  239. package/src/time/bounded-time.schema.test.ts +294 -0
  240. package/src/time/bounded-time.schema.ts +111 -0
  241. package/src/time/flexible-time-parser.test.ts +586 -0
  242. package/src/time/flexible-time-parser.ts +122 -0
  243. package/src/time/flexible-time.schema.test.ts +243 -0
  244. package/src/time/flexible-time.schema.ts +43 -0
  245. package/src/time/is-relative-time.test.ts +23 -0
  246. package/src/time/is-relative-time.ts +9 -0
  247. package/src/time/iso8601.schema.ts +29 -0
  248. package/src/time/iso8601.types.test.ts +112 -0
  249. package/src/time/iso8601.types.ts +21 -0
  250. package/src/time/parse-relative-time.test.ts +49 -0
  251. package/src/time/parse-relative-time.ts +50 -0
  252. package/src/time/relative-time.schema.test.ts +23 -0
  253. package/src/time/relative-time.schema.ts +38 -0
  254. package/src/time/since-parameter.schema.test.ts +59 -0
  255. package/src/time/since-parameter.schema.ts +69 -0
  256. package/src/time/time-helpers.test.ts +263 -0
  257. package/src/time/time-helpers.ts +78 -0
  258. package/src/time/time-schemas.test.ts +181 -0
  259. package/src/time/time-schemas.ts +42 -0
  260. package/src/time/time.schema.test.ts +237 -0
  261. package/src/time/timezone-independence.test.ts +188 -0
  262. package/src/time/timezone.types.test.ts +55 -0
  263. package/src/time/timezone.types.ts +22 -0
  264. package/tsconfig.json +26 -0
@@ -0,0 +1,1190 @@
1
+ # Contributing to @schafevormfenster/rest-commons
2
+
3
+ This guide explains **how to use** this package and **how to develop** proper REST API endpoints following our standards.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Quick Start](#quick-start)
8
+ - [Package Overview](#package-overview)
9
+ - [REST Service Design Patterns](#rest-service-design-patterns)
10
+ - [Technology Stack](#technology-stack)
11
+ - [Response Structure Patterns](#response-structure-patterns)
12
+ - [Contract-First Development](#contract-first-development)
13
+ - [Schema-Driven Validation](#schema-driven-validation)
14
+ - [Authentication Patterns](#authentication-patterns)
15
+ - [Caching Strategy](#caching-strategy)
16
+ - [Input Validation & Security](#input-validation--security)
17
+ - [Error Handling](#error-handling)
18
+ - [API Design Patterns](#api-design-patterns)
19
+ - [Unified GET/POST Endpoints](#unified-getpost-endpoints)
20
+ - [Static-First Processing](#static-first-processing)
21
+ - [Testing Guidelines](#testing-guidelines)
22
+ - [E2E Testing with Playwright](#e2e-testing-with-playwright)
23
+ - [Unit Testing](#unit-testing)
24
+ - [ESLint Rules for REST APIs](#eslint-rules-for-rest-apis)
25
+ - [Critical Rules](#critical-rules)
26
+ - [Important Rules](#important-rules)
27
+ - [Best Practice Rules](#best-practice-rules)
28
+ - [Development Workflow](#development-workflow)
29
+
30
+ ---
31
+
32
+ ## Quick Start
33
+
34
+ ### Installation
35
+
36
+ ```bash
37
+ pnpm add @schafevormfenster/rest-commons
38
+ ```
39
+
40
+ ### Basic Usage
41
+
42
+ ```typescript
43
+ // Import standardized response schemas
44
+ import {
45
+ ResultSchema,
46
+ ResultsSchema,
47
+ ErrorSchema,
48
+ HealthSchema,
49
+ OkaySchema,
50
+ } from "@schafevormfenster/rest-commons";
51
+
52
+ // Import primitive schemas
53
+ import {
54
+ LocationSchema,
55
+ SlugSchema,
56
+ UuidSchema,
57
+ } from "@schafevormfenster/rest-commons";
58
+
59
+ // Import time utilities
60
+ import {
61
+ ISO8601Schema,
62
+ parseRelativeTime,
63
+ isRelativeTime,
64
+ } from "@schafevormfenster/rest-commons";
65
+
66
+ // Import validation helpers
67
+ import {
68
+ detectSuspiciousPatterns,
69
+ normalizeList,
70
+ getCorrelationId,
71
+ } from "@schafevormfenster/rest-commons";
72
+ ```
73
+
74
+ ---
75
+
76
+ ## Package Overview
77
+
78
+ **@schafevormfenster/rest-commons** is the centralized authority for REST API standards and schemas. It provides:
79
+
80
+ - **Standardized Response Schemas**: Consistent wrappers for single resources, collections, errors, and health checks
81
+ - **Primitive Schemas**: Reusable Zod schemas for common data types (UUID, slug, location, etc.)
82
+ - **Time Utilities**: ISO 8601 handling, relative time parsing, week boundaries
83
+ - **Validation Helpers**: Input sanitization, suspicious pattern detection, parameter validation
84
+ - **Response Utilities**: Correlation IDs, header builders, environment detection
85
+
86
+ ### What This Package Contains
87
+
88
+ ```
89
+ src/
90
+ ├── api-schemas/ # Standardized response schemas
91
+ │ ├── result.schema.ts # Single resource response
92
+ │ ├── results.schema.ts # Collection response
93
+ │ ├── error.schema.ts # Error response
94
+ │ ├── health.schema.ts # Health check response
95
+ │ └── okay.schema.ts # Success acknowledgment
96
+ ├── primitives/ # Reusable primitive schemas
97
+ │ ├── location.schema.ts
98
+ │ ├── slug.schema.ts
99
+ │ ├── uuid.schema.ts
100
+ │ └── ...
101
+ ├── time/ # Time handling utilities
102
+ │ ├── iso8601.types.ts
103
+ │ ├── parse-relative-time.ts
104
+ │ └── ...
105
+ ├── helpers/ # Validation and utilities
106
+ │ ├── detect-suspicious-patterns.ts
107
+ │ ├── parameter-validation.ts
108
+ │ ├── correlation/
109
+ │ └── response-headers/
110
+ ├── normalization/ # Data normalization
111
+ └── validation/ # Zod utilities
112
+ ```
113
+
114
+ ---
115
+
116
+ ## REST Service Design Patterns
117
+
118
+ ### Technology Stack
119
+
120
+ Our REST APIs are built using:
121
+
122
+ - **ts-rest**: Contract-first API development
123
+ - **Zod**: Runtime schema validation and type safety
124
+ - **Next.js**: API routes and handlers
125
+ - **Fetch API**: HTTP client (axios is forbidden)
126
+ - **date-fns**: Date handling (moment is forbidden)
127
+
128
+ ⚠️ **Enforced by ESLint**: `stack/no-forbidden-imports` prevents usage of forbidden libraries.
129
+
130
+ ### Response Structure Patterns
131
+
132
+ All API endpoints must use standardized response schemas from this package:
133
+
134
+ #### Single Resource Response (`ResultSchema`)
135
+
136
+ Use for endpoints that return a single resource:
137
+
138
+ ```typescript
139
+ import { ResultSchema } from "@schafevormfenster/rest-commons";
140
+
141
+ // Define your data schema
142
+ const MyDataSchema = z.object({
143
+ id: z.string(),
144
+ name: z.string(),
145
+ });
146
+
147
+ // Response schema
148
+ const MyResponseSchema = ResultSchema.extend({
149
+ data: MyDataSchema,
150
+ });
151
+
152
+ export type MyResponse = z.infer<typeof MyResponseSchema>;
153
+
154
+ // Returns:
155
+ // {
156
+ // status: 200,
157
+ // timestamp: "2024-01-15T10:30:00.000Z",
158
+ // data: { id: "123", name: "Example" }
159
+ // }
160
+ ```
161
+
162
+ #### Collection Response (`ResultsSchema`)
163
+
164
+ Use for endpoints that return multiple resources:
165
+
166
+ ```typescript
167
+ import { ResultsSchema } from "@schafevormfenster/rest-commons";
168
+
169
+ const MyCollectionSchema = ResultsSchema.extend({
170
+ data: z.array(MyDataSchema),
171
+ });
172
+
173
+ // Returns:
174
+ // {
175
+ // status: 200,
176
+ // timestamp: "2024-01-15T10:30:00.000Z",
177
+ // data: [...]
178
+ // }
179
+ ```
180
+
181
+ #### Error Response (`ErrorSchema`)
182
+
183
+ All errors should use the standardized error schema:
184
+
185
+ ```typescript
186
+ import { ErrorSchema } from "@schafevormfenster/rest-commons";
187
+
188
+ // Returns:
189
+ // {
190
+ // status: 400,
191
+ // timestamp: "2024-01-15T10:30:00.000Z",
192
+ // error: "Validation error: Required field missing"
193
+ // }
194
+ ```
195
+
196
+ #### Success Response (`OkaySchema`)
197
+
198
+ For operations without data payload:
199
+
200
+ ```typescript
201
+ import { OkaySchema } from "@schafevormfenster/rest-commons";
202
+
203
+ // Returns:
204
+ // {
205
+ // status: 200,
206
+ // timestamp: "2024-01-15T10:30:00.000Z",
207
+ // message: "Operation completed successfully"
208
+ // }
209
+ ```
210
+
211
+ #### Health Check Response (`HealthSchema`)
212
+
213
+ For service monitoring:
214
+
215
+ ```typescript
216
+ import { HealthSchema } from "@schafevormfenster/rest-commons";
217
+
218
+ // Returns:
219
+ // {
220
+ // status: 200,
221
+ // timestamp: "2024-01-15T10:30:00.000Z",
222
+ // name: "My API",
223
+ // version: "1.0.0",
224
+ // dependencies: [...]
225
+ // }
226
+ ```
227
+
228
+ ### Contract-First Development
229
+
230
+ API contracts define the interface before implementation:
231
+
232
+ 1. **Define the contract** using ts-rest
233
+ 2. **Define Zod schemas** in `*.schema.ts` files
234
+ 3. **Implement route handlers** using the contract
235
+ 4. **Write tests** against the contract
236
+
237
+ ⚠️ **Enforced by ESLint**: `rest/enforce-api-route-structure` ensures every route has:
238
+ - `route.ts` - Route handler implementation
239
+ - `route.test.ts` - Co-located tests
240
+ - `*.contract.ts` - ts-rest API contract
241
+ - `*.schema.ts` - Zod schemas and TypeScript types
242
+
243
+ ### Schema-Driven Validation
244
+
245
+ Every Zod schema **must** have a corresponding TypeScript type export:
246
+
247
+ ```typescript
248
+ // ✅ CORRECT: Schema with TypeScript type export
249
+ export const UpdateRequestSchema = z.object({
250
+ id: z.string(),
251
+ name: z.string(),
252
+ });
253
+
254
+ export type UpdateRequest = z.infer<typeof UpdateRequestSchema>;
255
+ ```
256
+
257
+ ```typescript
258
+ // ❌ INCORRECT: Schema without TypeScript type export
259
+ export const UpdateRequestSchema = z.object({
260
+ id: z.string(),
261
+ name: z.string(),
262
+ });
263
+ // Missing: export type UpdateRequest = z.infer<typeof UpdateRequestSchema>;
264
+ ```
265
+
266
+ ⚠️ **Enforced by ESLint**: `rest/enforce-schema-type-export` ensures all schemas have type exports.
267
+
268
+ **Naming Convention**:
269
+ - Schema: `{Name}Schema` (e.g., `UpdateCalendarRequestSchema`)
270
+ - Type: `{Name}` (e.g., `UpdateCalendarRequest`)
271
+
272
+ **Reuse Common Schemas**:
273
+
274
+ ```typescript
275
+ // ✅ PREFERRED: Reuse standard response wrapper
276
+ import { ResultSchema } from "@schafevormfenster/rest-commons";
277
+
278
+ export const MyResponseSchema = ResultSchema.extend({
279
+ data: z.object({
280
+ message: z.string(),
281
+ }),
282
+ });
283
+ ```
284
+
285
+ ```typescript
286
+ // ❌ DISCOURAGED: Defining response structure from scratch
287
+ export const MyResponseSchema = z.object({
288
+ status: z.number(),
289
+ timestamp: z.string(),
290
+ data: z.object({
291
+ message: z.string(),
292
+ }),
293
+ });
294
+ ```
295
+
296
+ ⚠️ **Enforced by ESLint**: `rest/prefer-schema-extension` warns when you define schemas from scratch.
297
+
298
+ ---
299
+
300
+ ## Authentication Patterns
301
+
302
+ ### Token-as-Path Design
303
+
304
+ All authenticated endpoints require the token as a **path parameter**, not a header:
305
+
306
+ ✅ **Correct**: `POST /api/{token}/classify`
307
+ ❌ **Incorrect**: Using `Sheep-Token` header
308
+
309
+ **Implementation Pattern**:
310
+
311
+ ```typescript
312
+ import { checkAccessToken } from "@/security/auth";
313
+
314
+ export async function POST({ params }: { params: { token: string } }) {
315
+ // First action: validate token
316
+ checkAccessToken(params.token);
317
+
318
+ // Continue with route logic...
319
+ }
320
+ ```
321
+
322
+ **Error Responses**:
323
+ - `401 Unauthorized` - Token is missing, invalid, or not authorized
324
+
325
+ **Public Endpoints** (no token required):
326
+ - `GET /api/health`
327
+ - `GET /api/openapi`
328
+ - `GET /api/categories`
329
+ - `GET /api/scopes`
330
+
331
+ ⚠️ **Enforced by ESLint**: `rest/enforce-token-in-path` ensures proper authentication patterns.
332
+
333
+ ---
334
+
335
+ ## Caching Strategy
336
+
337
+ ### GET Requests
338
+
339
+ All GET route handlers **must** explicitly set `Cache-Control` headers:
340
+
341
+ ```typescript
342
+ export async function GET(request: Request) {
343
+ const response = Response.json({ data: [] });
344
+ response.headers.set('Cache-Control', 'public, max-age=3600');
345
+ return response;
346
+ }
347
+ ```
348
+
349
+ ⚠️ **Enforced by ESLint**: `caching/enforce-semantic-cache-headers` warns if Cache-Control is missing.
350
+
351
+ ### POST Requests
352
+
353
+ **Never** use `"use cache"` directive in POST handlers. Use `unstable_cache` instead:
354
+
355
+ ```typescript
356
+ // ❌ INCORRECT
357
+ "use cache";
358
+
359
+ export async function POST(request) {
360
+ // This won't work!
361
+ }
362
+ ```
363
+
364
+ ```typescript
365
+ // ✅ CORRECT
366
+ import { unstable_cache } from 'next/cache';
367
+
368
+ export async function POST(request) {
369
+ const cached = await unstable_cache(
370
+ async () => { /* ... */ },
371
+ ['cache-key']
372
+ )();
373
+ }
374
+ ```
375
+
376
+ ⚠️ **Enforced by ESLint**: `caching/no-use-cache-in-post` prevents incorrect cache usage.
377
+
378
+ ---
379
+
380
+ ## Input Validation & Security
381
+
382
+ ### Suspicious Pattern Detection
383
+
384
+ Always validate input for malicious patterns:
385
+
386
+ ```typescript
387
+ import { detectSuspiciousPatterns } from "@schafevormfenster/rest-commons";
388
+
389
+ export async function POST(request: Request) {
390
+ const body = await request.json();
391
+
392
+ // Detect suspicious patterns
393
+ if (detectSuspiciousPatterns(body.userInput)) {
394
+ return Response.json(
395
+ { status: 400, error: "Invalid input detected" },
396
+ { status: 400 }
397
+ );
398
+ }
399
+ }
400
+ ```
401
+
402
+ ### Parameter Validation
403
+
404
+ Use helper functions for common validation:
405
+
406
+ ```typescript
407
+ import { validateRequiredParams } from "@schafevormfenster/rest-commons";
408
+
409
+ export async function GET(request: Request) {
410
+ const { searchParams } = new URL(request.url);
411
+ const id = searchParams.get('id');
412
+
413
+ if (!validateRequiredParams({ id })) {
414
+ return Response.json(
415
+ { status: 400, error: "Missing required parameter: id" },
416
+ { status: 400 }
417
+ );
418
+ }
419
+ }
420
+ ```
421
+
422
+ ### External API Length Limitations
423
+
424
+ When integrating with external APIs, implement validation and truncation directly in the client function:
425
+
426
+ ```typescript
427
+ export const apiClientFunction = async (
428
+ query: QueryType
429
+ ): Promise<ResponseType> => {
430
+ // Truncate fields to respect external API's character limits
431
+ const processedQuery = {
432
+ ...query,
433
+ description:
434
+ query.description && query.description.length > 2000
435
+ ? query.description.substring(0, 2000)
436
+ : query.description,
437
+ };
438
+
439
+ // Log truncation events for monitoring
440
+ if (query.description && query.description.length > 2000) {
441
+ log.info(
442
+ {
443
+ originalLength: query.description.length,
444
+ truncatedLength: 2000,
445
+ },
446
+ "Description truncated for external API due to length constraint"
447
+ );
448
+ }
449
+
450
+ return await makeApiCall(processedQuery);
451
+ };
452
+ ```
453
+
454
+ ---
455
+
456
+ ## Error Handling
457
+
458
+ ### Centralized Error Processing
459
+
460
+ Use Zod error handlers for validation errors:
461
+
462
+ ```typescript
463
+ import { ZodError } from "zod";
464
+ import { ErrorSchema } from "@schafevormfenster/rest-commons";
465
+
466
+ try {
467
+ const validated = MySchema.parse(data);
468
+ } catch (error) {
469
+ if (error instanceof ZodError) {
470
+ return Response.json(
471
+ {
472
+ status: 400,
473
+ timestamp: new Date().toISOString(),
474
+ error: `Validation error: ${error.errors.map(e => e.message).join("; ")}`,
475
+ },
476
+ { status: 400 }
477
+ );
478
+ }
479
+ }
480
+ ```
481
+
482
+ ### HTTP Status Code Patterns
483
+
484
+ Use semantic HTTP status codes:
485
+
486
+ - **200 OK**: Successful request with data
487
+ - **201 Created**: Resource successfully created
488
+ - **202 Accepted**: Request accepted for processing (async)
489
+ - **400 Bad Request**: Client error (validation, malformed input)
490
+ - **401 Unauthorized**: Authentication required or failed
491
+ - **403 Forbidden**: Authenticated but not authorized
492
+ - **404 Not Found**: Resource not found
493
+ - **409 Conflict**: Resource already exists or conflict
494
+ - **500 Internal Server Error**: Server-side error
495
+
496
+ ### Correlation IDs
497
+
498
+ Include correlation IDs in all responses for request tracing:
499
+
500
+ ```typescript
501
+ import { getCorrelationId } from "@schafevormfenster/rest-commons";
502
+
503
+ export async function GET(request: Request) {
504
+ const correlationId = getCorrelationId(request);
505
+
506
+ const response = Response.json({ data: [] });
507
+ response.headers.set('X-Correlation-Id', correlationId);
508
+ return response;
509
+ }
510
+ ```
511
+
512
+ ---
513
+
514
+ ## API Design Patterns
515
+
516
+ ### Unified GET/POST Endpoints
517
+
518
+ Support identical request schemas for both GET and POST methods:
519
+
520
+ **GET Request** (simple parameters):
521
+ ```bash
522
+ GET /api/{token}/classify?tags=museum,kultur
523
+ ```
524
+
525
+ **POST Request** (rich context):
526
+ ```bash
527
+ POST /api/{token}/classify
528
+ {
529
+ "tags": ["museum", "kultur"],
530
+ "summary": "Annual city museum concert",
531
+ "description": "Classical music performance"
532
+ }
533
+ ```
534
+
535
+ **Implementation**:
536
+
537
+ ```typescript
538
+ // Flexible schema accepts both string and array formats
539
+ const TagsFieldSchema = z
540
+ .union([
541
+ z.string().transform((str) =>
542
+ str.split(",").map((s) => s.trim()).filter((s) => s.length > 0)
543
+ ),
544
+ z.array(z.string()),
545
+ ])
546
+ .optional()
547
+ .default([]);
548
+ ```
549
+
550
+ **When to Use GET vs POST**:
551
+ - **GET**: Simple queries, caching, bookmarkable URLs
552
+ - **POST**: Rich context, large payloads, sensitive data
553
+
554
+ ### Static-First Processing
555
+
556
+ Optimize performance by checking static mappings before calling AI services:
557
+
558
+ 1. **Static Matching**: Check against predefined dictionary
559
+ 2. **AI Fallback**: Use AI only when no static match exists
560
+
561
+ **Benefits**:
562
+ - Fast response (no external API calls)
563
+ - Cost reduction (fewer AI API calls)
564
+ - Deterministic results (consistent outcomes)
565
+
566
+ **Example**:
567
+ ```typescript
568
+ // 1. Try static matching first
569
+ const staticResult = staticMappingService.lookup(query);
570
+
571
+ if (staticResult) {
572
+ log.info({ query }, "Static match found");
573
+ return staticResult;
574
+ }
575
+
576
+ // 2. Fall back to AI service
577
+ log.info({ query }, "No static match, using AI");
578
+ return await aiService.classify(query);
579
+ ```
580
+
581
+ ---
582
+
583
+ ## Testing Guidelines
584
+
585
+ ### E2E Testing with Playwright
586
+
587
+ E2E tests use Playwright's request context to test HTTP endpoints directly.
588
+
589
+ #### Environment Configuration
590
+
591
+ **Use ONLY `APITEST_*` environment variables**:
592
+
593
+ ```typescript
594
+ // ✅ Correct - Use APITEST_* env vars directly
595
+ const baseURL = process.env.APITEST_BASE_URL;
596
+ const readToken = process.env.APITEST_READ_ACCESS_TOKEN;
597
+
598
+ // ❌ Wrong - Don't use production tokens
599
+ const readToken = process.env.READ_ACCESS_TOKENS?.split(",")[0];
600
+ ```
601
+
602
+ ⚠️ **Enforced by ESLint**: `testing/enforce-apitest-env-vars` prevents incorrect environment variable usage.
603
+
604
+ #### Required Environment Variables
605
+
606
+ Create `.env.local` for development:
607
+
608
+ ```bash
609
+ APITEST_BASE_URL=http://localhost:8000
610
+ APITEST_READ_ACCESS_TOKEN=test-read-token
611
+ APITEST_WRITE_ACCESS_TOKEN=test-write-token
612
+ APITEST_ADMIN_ACCESS_TOKEN=test-admin-token
613
+ ```
614
+
615
+ #### Test Commands
616
+
617
+ ```bash
618
+ # Local development
619
+ pnpm e2e
620
+
621
+ # Production environment
622
+ pnpm e2e:prod
623
+
624
+ # CI environment
625
+ pnpm e2e:ci
626
+ ```
627
+
628
+ #### Test Organization
629
+
630
+ ```text
631
+ e2e/
632
+ ├── health.spec.ts # Health endpoint tests
633
+ ├── auth-access-tokens.spec.ts # Authentication tests
634
+ ├── categories.spec.ts # Categories API tests
635
+ └── datasets/ # Large dataset tests (tagged @datasets)
636
+ └── sample-dataset.spec.ts
637
+ ```
638
+
639
+ #### Error Message Matching
640
+
641
+ Use **loose regex matching** for error messages:
642
+
643
+ ```typescript
644
+ // ✅ CORRECT - Loose comparison with regex
645
+ test("should return validation error", async ({ request }) => {
646
+ const response = await request.post("/api/events", {
647
+ data: { invalid: "data" },
648
+ });
649
+
650
+ const body = await response.json();
651
+ expect(body.error).toMatch(/validation\s+error/i);
652
+ });
653
+
654
+ // ❌ WRONG - Exact string matching (brittle)
655
+ test("should return validation error", async ({ request }) => {
656
+ const response = await request.post("/api/events", {
657
+ data: { invalid: "data" },
658
+ });
659
+
660
+ const body = await response.json();
661
+ expect(body.error).toContain("Validation Error"); // Breaks if case changes
662
+ });
663
+ ```
664
+
665
+ ⚠️ **Enforced by ESLint**: `testing/prefer-loose-error-matching` encourages flexible error matching.
666
+
667
+ #### Test File Naming
668
+
669
+ ```typescript
670
+ // ✅ CORRECT - Playwright tests use .spec.ts
671
+ // e2e/health.spec.ts
672
+
673
+ test("should return health status", async ({ request }) => {
674
+ const response = await request.get("/api/health");
675
+ expect(response.status()).toBe(200);
676
+ });
677
+ ```
678
+
679
+ ⚠️ **Enforced by ESLint**: `testing/enforce-test-file-naming` ensures proper naming conventions.
680
+
681
+ ### Unit Testing
682
+
683
+ Unit tests use Vitest and should focus on business logic:
684
+
685
+ ```typescript
686
+ // ✅ CORRECT - Vitest tests use .test.ts
687
+ // src/helpers/slugify.test.ts
688
+
689
+ import { describe, it, expect } from "vitest";
690
+ import { slugify } from "./slugify";
691
+
692
+ describe("slugify", () => {
693
+ it("should convert text to slug", () => {
694
+ expect(slugify("Hello World")).toBe("hello-world");
695
+ });
696
+ });
697
+ ```
698
+
699
+ ---
700
+
701
+ ## ESLint Rules for REST APIs
702
+
703
+ This section details the most important ESLint rules for REST API development. These rules are automatically enforced when you use `@schafevormfenster/eslint-config`.
704
+
705
+ ### Critical Rules
706
+
707
+ These rules are **enabled as errors** and will break your build if violated:
708
+
709
+ #### 1. `rest/enforce-api-route-structure`
710
+
711
+ **Purpose**: Ensures every API route has required files (contract, schema, tests).
712
+
713
+ **What it checks**:
714
+ - Every `route.ts` must have a co-located `route.test.ts`
715
+ - Every route directory must have a `*.contract.ts` file
716
+ - Every route directory must have a `*.schema.ts` file
717
+
718
+ **Example**:
719
+ ```text
720
+ ✅ Correct structure:
721
+ app/api/[token]/classify/
722
+ ├── route.ts
723
+ ├── route.test.ts
724
+ ├── classify.contract.ts
725
+ └── classify.schema.ts
726
+
727
+ ❌ Incorrect structure:
728
+ app/api/classify/
729
+ └── route.ts # Missing required files!
730
+ ```
731
+
732
+ **Why it matters**: Enforces contract-first development and ensures comprehensive test coverage.
733
+
734
+ #### 2. `rest/enforce-schema-type-export`
735
+
736
+ **Purpose**: Every Zod schema must have a TypeScript type export.
737
+
738
+ **What it checks**:
739
+ - All exported Zod schemas in `*.schema.ts` files
740
+ - Ensures `z.infer<typeof Schema>` type is exported
741
+
742
+ **Example**:
743
+ ```typescript
744
+ // ✅ Correct
745
+ export const UserSchema = z.object({ id: z.string() });
746
+ export type User = z.infer<typeof UserSchema>;
747
+
748
+ // ❌ Incorrect
749
+ export const UserSchema = z.object({ id: z.string() });
750
+ ```
751
+
752
+ **Why it matters**: Maintains type safety and ensures consistent type usage across the codebase.
753
+
754
+ #### 3. `rest/enforce-token-in-path`
755
+
756
+ **Purpose**: Enforces token-based authentication in API routes.
757
+
758
+ **What it checks**:
759
+ - Routes include `[token]` in path OR validate `params.token` using `checkAccessToken()`
760
+ - Public routes (health, openapi, etc.) are automatically excluded
761
+
762
+ **Example**:
763
+ ```typescript
764
+ // ✅ Correct
765
+ export async function GET({ params }: { params: { token: string } }) {
766
+ checkAccessToken(params.token);
767
+ // ...
768
+ }
769
+
770
+ // ❌ Incorrect
771
+ export async function GET(request: Request) {
772
+ // No token validation!
773
+ }
774
+ ```
775
+
776
+ **Why it matters**: Ensures proper authentication on all protected endpoints.
777
+
778
+ #### 4. `caching/no-use-cache-in-post`
779
+
780
+ **Purpose**: Prevents incorrect cache directive usage in POST handlers.
781
+
782
+ **What it checks**:
783
+ - No `"use cache"` directive in POST request handlers
784
+
785
+ **Example**:
786
+ ```typescript
787
+ // ✅ Correct
788
+ import { unstable_cache } from 'next/cache';
789
+ export async function POST(request) {
790
+ const cached = await unstable_cache(async () => {...}, ['key'])();
791
+ }
792
+
793
+ // ❌ Incorrect
794
+ "use cache";
795
+ export async function POST(request) { ... }
796
+ ```
797
+
798
+ **Why it matters**: `"use cache"` doesn't work with POST requests; prevents subtle bugs.
799
+
800
+ ### Important Rules
801
+
802
+ These rules are **enabled as warnings** and should be addressed:
803
+
804
+ #### 5. `rest/prefer-schema-extension`
805
+
806
+ **Purpose**: Encourages reusing standard response schemas instead of defining from scratch.
807
+
808
+ **What it checks**:
809
+ - Warns when defining schemas with common keys (status, timestamp, data) from scratch
810
+ - Suggests using `.extend()` or `.merge()` with standard schemas
811
+
812
+ **Example**:
813
+ ```typescript
814
+ // ✅ Preferred
815
+ import { ResultSchema } from "@schafevormfenster/rest-commons";
816
+ export const MyResponseSchema = ResultSchema.extend({
817
+ data: MyDataSchema,
818
+ });
819
+
820
+ // ⚠️ Warning
821
+ export const MyResponseSchema = z.object({
822
+ status: z.number(),
823
+ timestamp: z.string(),
824
+ data: MyDataSchema,
825
+ });
826
+ ```
827
+
828
+ **Why it matters**: Maintains consistent response structures across the API.
829
+
830
+ #### 6. `caching/enforce-semantic-cache-headers`
831
+
832
+ **Purpose**: Ensures proper cache control headers in GET requests.
833
+
834
+ **What it checks**:
835
+ - All GET route handlers set `Cache-Control` header
836
+
837
+ **Example**:
838
+ ```typescript
839
+ // ✅ Correct
840
+ export async function GET(request) {
841
+ const response = Response.json({ data: [] });
842
+ response.headers.set('Cache-Control', 'public, max-age=3600');
843
+ return response;
844
+ }
845
+
846
+ // ⚠️ Warning
847
+ export async function GET(request) {
848
+ return Response.json({ data: [] }); // Missing Cache-Control
849
+ }
850
+ ```
851
+
852
+ **Why it matters**: Proper caching improves performance and prevents unintended caching issues.
853
+
854
+ ### Best Practice Rules
855
+
856
+ These rules help maintain code quality:
857
+
858
+ #### 7. `testing/enforce-apitest-env-vars`
859
+
860
+ **Purpose**: Ensures E2E tests use isolated test environment variables.
861
+
862
+ **What it checks**:
863
+ - E2E tests only access `APITEST_*` environment variables
864
+ - Prevents accidental use of production tokens
865
+
866
+ **Example**:
867
+ ```typescript
868
+ // ✅ Correct
869
+ const baseURL = process.env.APITEST_BASE_URL;
870
+
871
+ // ❌ Incorrect
872
+ const baseURL = process.env.BASE_URL || "http://localhost:8000";
873
+ ```
874
+
875
+ **Why it matters**: Prevents accidentally running tests against production.
876
+
877
+ #### 8. `testing/prefer-loose-error-matching`
878
+
879
+ **Purpose**: Encourages flexible error message matching in tests.
880
+
881
+ **What it checks**:
882
+ - Warns about exact string matching for error messages
883
+ - Suggests regex-based matching
884
+
885
+ **Example**:
886
+ ```typescript
887
+ // ✅ Correct
888
+ expect(body.error).toMatch(/validation\s+error/i);
889
+
890
+ // ⚠️ Warning
891
+ expect(body.error).toContain("Validation Error");
892
+ ```
893
+
894
+ **Why it matters**: Tests remain stable when error messages are improved.
895
+
896
+ #### 9. `testing/enforce-test-file-naming`
897
+
898
+ **Purpose**: Enforces correct test file naming conventions.
899
+
900
+ **What it checks**:
901
+ - Playwright tests use `*.spec.ts`
902
+ - Vitest tests use `*.test.ts`
903
+
904
+ **Example**:
905
+ ```text
906
+ ✅ Correct:
907
+ e2e/health.spec.ts # Playwright
908
+ src/helpers/slugify.test.ts # Vitest
909
+
910
+ ❌ Incorrect:
911
+ e2e/health.test.ts # Should be .spec.ts
912
+ src/helpers/slugify.spec.ts # Should be .test.ts
913
+ ```
914
+
915
+ **Why it matters**: Maintains consistent naming conventions and helps test runners find tests.
916
+
917
+ #### 10. `stack/no-forbidden-imports`
918
+
919
+ **Purpose**: Prevents usage of forbidden libraries with preferred alternatives.
920
+
921
+ **What it checks**:
922
+ - Forbids `axios` (use Fetch API)
923
+ - Forbids `moment` (use date-fns)
924
+ - Forbids `jest` (use Vitest)
925
+
926
+ **Example**:
927
+ ```typescript
928
+ // ✅ Correct
929
+ const response = await fetch(url);
930
+
931
+ // ❌ Incorrect
932
+ import axios from 'axios';
933
+ const response = await axios.get(url);
934
+ ```
935
+
936
+ **Why it matters**: Enforces consistent tech stack choices across the monorepo.
937
+
938
+ ### Enabling ESLint Rules
939
+
940
+ In your `eslint.config.mjs`:
941
+
942
+ ```javascript
943
+ import svfNext from "@schafevormfenster/eslint-config/next";
944
+ import svfRest from "@schafevormfenster/eslint-config/rest";
945
+ import svfCaching from "@schafevormfenster/eslint-config/caching";
946
+ import svfPlaywright from "@schafevormfenster/eslint-config/playwright";
947
+
948
+ export default [
949
+ ...svfNext, // Base Next.js rules
950
+ ...svfRest, // REST API rules (includes critical rules)
951
+ ...svfCaching, // Caching rules
952
+ ...svfPlaywright, // E2E testing rules (only for e2e/ directory)
953
+ ];
954
+ ```
955
+
956
+ ---
957
+
958
+ ## Development Workflow
959
+
960
+ ### 1. Create API Endpoint Structure
961
+
962
+ ```bash
963
+ # Create route directory
964
+ mkdir -p app/api/[token]/my-endpoint
965
+
966
+ # Create required files
967
+ touch app/api/[token]/my-endpoint/route.ts
968
+ touch app/api/[token]/my-endpoint/route.test.ts
969
+ touch app/api/[token]/my-endpoint/my-endpoint.contract.ts
970
+ touch app/api/[token]/my-endpoint/my-endpoint.schema.ts
971
+ ```
972
+
973
+ ### 2. Define Zod Schemas
974
+
975
+ ```typescript
976
+ // my-endpoint.schema.ts
977
+ import { z } from "zod";
978
+ import { ResultSchema } from "@schafevormfenster/rest-commons";
979
+
980
+ export const MyRequestSchema = z.object({
981
+ id: z.string(),
982
+ name: z.string(),
983
+ });
984
+
985
+ export type MyRequest = z.infer<typeof MyRequestSchema>;
986
+
987
+ export const MyDataSchema = z.object({
988
+ id: z.string(),
989
+ name: z.string(),
990
+ createdAt: z.string(),
991
+ });
992
+
993
+ export type MyData = z.infer<typeof MyDataSchema>;
994
+
995
+ export const MyResponseSchema = ResultSchema.extend({
996
+ data: MyDataSchema,
997
+ });
998
+
999
+ export type MyResponse = z.infer<typeof MyResponseSchema>;
1000
+ ```
1001
+
1002
+ ### 3. Define API Contract
1003
+
1004
+ ```typescript
1005
+ // my-endpoint.contract.ts
1006
+ import { initContract } from "@ts-rest/core";
1007
+ import { MyRequestSchema, MyResponseSchema } from "./my-endpoint.schema";
1008
+
1009
+ const c = initContract();
1010
+
1011
+ export const myEndpointContract = c.router({
1012
+ get: {
1013
+ method: "GET",
1014
+ path: "/api/:token/my-endpoint/:id",
1015
+ responses: {
1016
+ 200: MyResponseSchema,
1017
+ 400: ErrorSchema,
1018
+ 401: ErrorSchema,
1019
+ },
1020
+ },
1021
+ post: {
1022
+ method: "POST",
1023
+ path: "/api/:token/my-endpoint",
1024
+ body: MyRequestSchema,
1025
+ responses: {
1026
+ 201: MyResponseSchema,
1027
+ 400: ErrorSchema,
1028
+ 401: ErrorSchema,
1029
+ },
1030
+ },
1031
+ });
1032
+ ```
1033
+
1034
+ ### 4. Implement Route Handler
1035
+
1036
+ ```typescript
1037
+ // route.ts
1038
+ import { checkAccessToken } from "@/security/auth";
1039
+ import { getCorrelationId } from "@schafevormfenster/rest-commons";
1040
+ import { MyRequestSchema } from "./my-endpoint.schema";
1041
+
1042
+ export async function GET({
1043
+ params,
1044
+ }: {
1045
+ params: { token: string; id: string };
1046
+ }) {
1047
+ checkAccessToken(params.token);
1048
+
1049
+ const correlationId = getCorrelationId();
1050
+
1051
+ // Business logic here
1052
+ const data = await fetchData(params.id);
1053
+
1054
+ const response = Response.json({
1055
+ status: 200,
1056
+ timestamp: new Date().toISOString(),
1057
+ data,
1058
+ });
1059
+
1060
+ response.headers.set('X-Correlation-Id', correlationId);
1061
+ response.headers.set('Cache-Control', 'public, max-age=3600');
1062
+
1063
+ return response;
1064
+ }
1065
+
1066
+ export async function POST({
1067
+ params,
1068
+ request,
1069
+ }: {
1070
+ params: { token: string };
1071
+ request: Request;
1072
+ }) {
1073
+ checkAccessToken(params.token);
1074
+
1075
+ const body = await request.json();
1076
+ const validated = MyRequestSchema.parse(body);
1077
+
1078
+ // Business logic here
1079
+ const data = await createData(validated);
1080
+
1081
+ return Response.json(
1082
+ {
1083
+ status: 201,
1084
+ timestamp: new Date().toISOString(),
1085
+ data,
1086
+ },
1087
+ { status: 201 }
1088
+ );
1089
+ }
1090
+ ```
1091
+
1092
+ ### 5. Write Tests
1093
+
1094
+ ```typescript
1095
+ // route.test.ts
1096
+ import { test, expect } from "@playwright/test";
1097
+
1098
+ const READ_TOKEN = process.env.APITEST_READ_ACCESS_TOKEN;
1099
+ const WRITE_TOKEN = process.env.APITEST_WRITE_ACCESS_TOKEN;
1100
+
1101
+ test.describe("My Endpoint API", () => {
1102
+ test("GET requires authentication", async ({ request }) => {
1103
+ const response = await request.get("/api/invalid-token/my-endpoint/123");
1104
+ expect(response.status()).toBe(401);
1105
+ });
1106
+
1107
+ test("GET returns data with valid token", async ({ request }) => {
1108
+ const response = await request.get(
1109
+ `/api/${READ_TOKEN}/my-endpoint/123`
1110
+ );
1111
+
1112
+ expect(response.status()).toBe(200);
1113
+
1114
+ const body = await response.json();
1115
+ expect(body).toHaveProperty("status", 200);
1116
+ expect(body).toHaveProperty("timestamp");
1117
+ expect(body).toHaveProperty("data");
1118
+ });
1119
+
1120
+ test("POST creates resource", async ({ request }) => {
1121
+ const response = await request.post(`/api/${WRITE_TOKEN}/my-endpoint`, {
1122
+ data: {
1123
+ id: "123",
1124
+ name: "Test",
1125
+ },
1126
+ });
1127
+
1128
+ expect(response.status()).toBe(201);
1129
+ });
1130
+
1131
+ test("POST validates input", async ({ request }) => {
1132
+ const response = await request.post(`/api/${WRITE_TOKEN}/my-endpoint`, {
1133
+ data: { invalid: "data" },
1134
+ });
1135
+
1136
+ expect(response.status()).toBe(400);
1137
+
1138
+ const body = await response.json();
1139
+ expect(body.error).toMatch(/validation/i);
1140
+ });
1141
+ });
1142
+ ```
1143
+
1144
+ ### 6. Run Linter and Tests
1145
+
1146
+ ```bash
1147
+ # Run ESLint
1148
+ pnpm lint
1149
+
1150
+ # Run unit tests
1151
+ pnpm test
1152
+
1153
+ # Run E2E tests
1154
+ pnpm e2e
1155
+ ```
1156
+
1157
+ ### 7. Build and Deploy
1158
+
1159
+ ```bash
1160
+ # Build the package
1161
+ pnpm build
1162
+
1163
+ # The package is ready to publish
1164
+ ```
1165
+
1166
+ ---
1167
+
1168
+ ## Additional Resources
1169
+
1170
+ - **REST Service Design Guide**: See `/packages/eslint-plugin/docs/rest-service-design-guide.md`
1171
+ - **E2E Testing Guide**: See `/packages/eslint-plugin/docs/instructions/rest-api-e2e-testing-guide.md`
1172
+ - **Tech Stack**: See `/packages/eslint-plugin/docs/instructions/tech-stack.md`
1173
+ - **ESLint Plugin**: See `/packages/eslint-plugin/README.md`
1174
+ - **ESLint Config**: See `/packages/eslint-config/README.md`
1175
+
1176
+ ---
1177
+
1178
+ ## Getting Help
1179
+
1180
+ If you have questions or need help:
1181
+
1182
+ 1. Check the documentation in `/packages/eslint-plugin/docs/`
1183
+ 2. Review existing implementations in the monorepo
1184
+ 3. Ask in the team chat or create an issue
1185
+
1186
+ ---
1187
+
1188
+ ## License
1189
+
1190
+ See the root LICENSE file for license information.