@wenathlan/saddle 1.8.2 → 1.8.5

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 (257) hide show
  1. package/README.md +9 -9
  2. package/browser/playwright.js +22 -0
  3. package/docs/.gitkeep +0 -0
  4. package/docs/gapmatrix.md +3 -3
  5. package/docs/libraryapi.md +1 -0
  6. package/docs/logs/.gitkeep +0 -0
  7. package/docs/packageaudit185.md +23 -0
  8. package/docs/plans/00.index.md +50 -0
  9. package/docs/plans/01.architecture.md +86 -0
  10. package/docs/plans/02.research.computer.use.md +58 -0
  11. package/docs/plans/03.research.captcha.bypass.md +68 -0
  12. package/docs/plans/04.research.sandbox.ai.md +52 -0
  13. package/docs/plans/05.capture.platform.md +57 -0
  14. package/docs/plans/06.dependencies.md +97 -0
  15. package/docs/plans/07.captcha.test.page.md +41 -0
  16. package/docs/plans/08.production.infra.md +70 -0
  17. package/docs/plans/09.database.schema.md +121 -0
  18. package/docs/plans/10.cloudinary.storage.md +57 -0
  19. package/docs/plans/11.movement.logs.json.md +72 -0
  20. package/docs/plans/12.research.atlas.agent.browser.md +79 -0
  21. package/docs/plans/13.research.anti.detection.md +898 -0
  22. package/docs/plans/14.research.proxy.md +1495 -0
  23. package/docs/plans/15.research.retry.rate.limit.md +1958 -0
  24. package/docs/plans/16.research.crawling.md +1417 -0
  25. package/docs/plans/17.research.caching.md +1610 -0
  26. package/docs/plans/18.research.content.extraction.md +1952 -0
  27. package/docs/plans/19.research.errors.events.md +1523 -0
  28. package/docs/plans/20.research.zod.validation.md +1350 -0
  29. package/docs/plans/21.research.batch.concurrency.md +1888 -0
  30. package/docs/plans/22.research.universal.runtime.md +944 -0
  31. package/docs/plans/23.research.ai.integration.md +1465 -0
  32. package/docs/plans/24.research.memory.persistence.md +1979 -0
  33. package/docs/plans/25.research.server.api.md +342 -0
  34. package/docs/plans/26.research.compilation.md +249 -0
  35. package/docs/plans/27.research.html.parsing.md +251 -0
  36. package/docs/plans/28.action.plan.md +50 -0
  37. package/docs/plans/29.api.reference.md +174 -0
  38. package/docs/plans/30.architecture.plan.md +94 -0
  39. package/docs/plans/31.auditoria.dados.md +163 -0
  40. package/docs/plans/32.bots.automacao.computacional.md +214 -0
  41. package/docs/plans/33.bots.codigo.revisao.md +220 -0
  42. package/docs/plans/34.bots.seguranca.cicd.md +366 -0
  43. package/docs/plans/35.comparativo.concorrencia.md +464 -0
  44. package/docs/plans/36.computational.memory.md +340 -0
  45. package/docs/plans/37.deploystrategy.md +394 -0
  46. package/docs/plans/38.flow.md +155 -0
  47. package/docs/plans/39.multi.platform.bot.md +252 -0
  48. package/docs/plans/40.npm.publish.md +250 -0
  49. package/docs/plans/41.o.que.falta.md +407 -0
  50. package/docs/plans/42.pesquisa.concorrencia.md +721 -0
  51. package/docs/plans/43.plan.universal.architecture.md +496 -0
  52. package/docs/plans/44.reference.md +100 -0
  53. package/docs/plans/45.robotarchitecture.md +237 -0
  54. package/docs/plans/46.scdnintegration.md +284 -0
  55. package/docs/plans/47.multiforge.readme.md +129 -0
  56. package/docs/plans/48.theory.v4.repo.os.md +152 -0
  57. package/docs/plans/49.third.party.infra.md +12 -0
  58. package/docs/plans/50.file.as.compute.md +39 -0
  59. package/docs/plans/51.architecture.virtual.processor.md +80 -0
  60. package/docs/plans/52.manifesto.v8.md +11 -0
  61. package/docs/plans/58.cdn.list.md +23 -0
  62. package/docs/plans/59.sql.frameworks.md +33 -0
  63. package/docs/plans/60.sql.thirdparty.md +26 -0
  64. package/docs/plans/61.objective.multiforge.md +63 -0
  65. package/docs/plans/62.huggingface.upload.md +26 -0
  66. package/docs/plans/63.kaggle.upload.md +24 -0
  67. package/docs/plans/64.npm.storage.md +30 -0
  68. package/docs/plans/65.rclone.terabox.md +32 -0
  69. package/docs/plans/66.buckets.and.models.todo.md +14 -0
  70. package/docs/plans/67.database.todo.md +13 -0
  71. package/docs/plans/68.deploy.packages.todo.md +12 -0
  72. package/docs/plans/69.report.human.operator.md +133 -0
  73. package/docs/plans/70.report.brain2qwerty.ems.md +135 -0
  74. package/docs/plans/71.report.hd.infinito.vram.md +155 -0
  75. package/docs/plans/72.plan.hd.infinito.node.md +146 -0
  76. package/docs/plans/73.plan.scifi.repos.md +125 -0
  77. package/docs/plans/74.000.manifesto.v8.flat.2..md +11 -0
  78. package/docs/plans/README.md +489 -0
  79. package/docs/plans/aggregate_platforms.mjs +146 -0
  80. package/docs/plans/examplesession.json +36 -0
  81. package/docs/plans/missing-facts.md +192 -0
  82. package/docs/plans/models.md +64 -0
  83. package/docs/plans/organize.cjs +270 -0
  84. package/docs/plans/platforms.md +2887 -0
  85. package/docs/plans/sites.md +31322 -0
  86. package/docs/platformpipelineaudit.md +18 -0
  87. package/docs/platformpipelines.md +13 -0
  88. package/docs/registryresearch.md +4 -0
  89. package/docs/release.md +4 -4
  90. package/docs/release182notes.md +2 -0
  91. package/docs/release184notes.md +7 -0
  92. package/docs/release185notes.md +7 -0
  93. package/docs/sources/farm.py +117 -0
  94. package/docs/sources/html/saddle1.html +132 -0
  95. package/docs/sources/html/saddle2.html +157 -0
  96. package/docs/sources/html/saddle3.html +119 -0
  97. package/docs/sources/html/saddle4.html +144 -0
  98. package/docs/sources/html/saddle5.html +72 -0
  99. package/docs/sources/html/saddle6.html +171 -0
  100. package/docs/sources/html/saddle7.html +236 -0
  101. package/docs/sources/saddle.ts +74 -0
  102. package/docs/sources/schema.prisma +88 -0
  103. package/docs/sources/script.sh +64 -0
  104. package/docs/sources/workflows.yml +458 -0
  105. package/docs/talks1/_body.txt +14 -0
  106. package/docs/talks1/_index.md +15 -0
  107. package/docs/talks1/_screenshot.png +0 -0
  108. package/docs/talks1/assistant-01.md +5 -0
  109. package/docs/talks1/assistant-02.md +5 -0
  110. package/docs/talks1/assistant-03.md +531 -0
  111. package/docs/talks1/assistant-04.md +26 -0
  112. package/docs/talks1/assistant-05.md +774 -0
  113. package/docs/talks1/assistant-06.md +1718 -0
  114. package/docs/talks1/scrape-share.cjs +185 -0
  115. package/docs/talks1/scrape-share.ts +183 -0
  116. package/docs/talks1/user-01.md +3 -0
  117. package/docs/talks1/user-02.md +3 -0
  118. package/docs/talks1/user-03.md +88 -0
  119. package/docs/talks1/user-04.md +3 -0
  120. package/docs/talks1/user-05.md +3 -0
  121. package/docs/talks1/user-06.md +88 -0
  122. package/docs/talks1/user-07.md +88 -0
  123. package/docs/talks2/_body.txt +14 -0
  124. package/docs/talks2/_index.md +16 -0
  125. package/docs/talks2/_screenshot.png +0 -0
  126. package/docs/talks2/assistant-01.md +5 -0
  127. package/docs/talks2/assistant-02.md +5 -0
  128. package/docs/talks2/assistant-03.md +424 -0
  129. package/docs/talks2/assistant-04.md +598 -0
  130. package/docs/talks2/assistant-05.md +1280 -0
  131. package/docs/talks2/assistant-06.md +1227 -0
  132. package/docs/talks2/assistant-07.md +1252 -0
  133. package/docs/talks2/user-01.md +3 -0
  134. package/docs/talks2/user-02.md +3 -0
  135. package/docs/talks2/user-03.md +88 -0
  136. package/docs/talks2/user-04.md +88 -0
  137. package/docs/talks2/user-05.md +88 -0
  138. package/docs/talks2/user-06.md +88 -0
  139. package/docs/talks2/user-07.md +3 -0
  140. package/docs/talks3/_body.txt +467 -0
  141. package/docs/talks3/_index.md +10 -0
  142. package/docs/talks3/_screenshot.png +0 -0
  143. package/docs/talks3/assistant-01.md +417 -0
  144. package/docs/talks3/assistant-02.md +417 -0
  145. package/docs/talks3/assistant-03.md +29 -0
  146. package/docs/talks3/assistant-04.md +727 -0
  147. package/docs/talks3/user-01.md +88 -0
  148. package/docs/talks3/user-02.md +88 -0
  149. package/docs/talks3/user-03.md +3 -0
  150. package/docs/talks3/user-04.md +3 -0
  151. package/docs/talks4/_body.txt +14 -0
  152. package/docs/talks4/_index.md +12 -0
  153. package/docs/talks4/_screenshot.png +0 -0
  154. package/docs/talks4/assistant-01.md +5 -0
  155. package/docs/talks4/assistant-02.md +5 -0
  156. package/docs/talks4/assistant-03.md +35 -0
  157. package/docs/talks4/assistant-04.md +512 -0
  158. package/docs/talks4/assistant-05.md +599 -0
  159. package/docs/talks4/user-01.md +3 -0
  160. package/docs/talks4/user-02.md +3 -0
  161. package/docs/talks4/user-03.md +88 -0
  162. package/docs/talks4/user-04.md +88 -0
  163. package/docs/talks4/user-05.md +7 -0
  164. package/docs/talks5/_body.txt +14 -0
  165. package/docs/talks5/_index.md +13 -0
  166. package/docs/talks5/_screenshot.png +0 -0
  167. package/docs/talks5/assistant-01.md +5 -0
  168. package/docs/talks5/assistant-02.md +5 -0
  169. package/docs/talks5/assistant-03.md +690 -0
  170. package/docs/talks5/assistant-04.md +758 -0
  171. package/docs/talks5/assistant-05.md +974 -0
  172. package/docs/talks5/user-01.md +3 -0
  173. package/docs/talks5/user-02.md +3 -0
  174. package/docs/talks5/user-03.md +105 -0
  175. package/docs/talks5/user-04.md +105 -0
  176. package/docs/talks5/user-05.md +63 -0
  177. package/docs/talks5/user-06.md +105 -0
  178. package/docs/talks6/_body.txt +14 -0
  179. package/docs/talks6/_index.md +9 -0
  180. package/docs/talks6/_screenshot.png +0 -0
  181. package/docs/talks6/assistant-01.md +5 -0
  182. package/docs/talks6/assistant-02.md +5 -0
  183. package/docs/talks6/assistant-03.md +1499 -0
  184. package/docs/talks6/user-01.md +3 -0
  185. package/docs/talks6/user-02.md +3 -0
  186. package/docs/talks6/user-03.md +88 -0
  187. package/docs/talks6/user-04.md +88 -0
  188. package/docs/talks7/_body.txt +14 -0
  189. package/docs/talks7/_index.md +10 -0
  190. package/docs/talks7/_screenshot.png +0 -0
  191. package/docs/talks7/assistant-01.md +5 -0
  192. package/docs/talks7/assistant-02.md +5 -0
  193. package/docs/talks7/assistant-03.md +523 -0
  194. package/docs/talks7/assistant-04.md +617 -0
  195. package/docs/talks7/user-01.md +3 -0
  196. package/docs/talks7/user-02.md +3 -0
  197. package/docs/talks7/user-03.md +105 -0
  198. package/docs/talks7/user-04.md +67 -0
  199. package/docs/talks8/conversa1.txt +1322 -0
  200. package/docs/talks8/conversa2.txt +237 -0
  201. package/docs/talks9/Beyond the Obvious_ 50 Plataformas Auto-Hospedadas de Forja de C/303/263digo para Al/303/251m de Gitea e GitLab.md" +174 -0
  202. package/docs/talks9/De NPM a Multi-Linguagem_ Uma Arquitetura T/303/251cnica para a Execu/303/247/303/243o Integrada de C/303/263digo no Ecossistema Node.js.md" +59 -0
  203. package/docs/talks9/De NPM a VMs Virtuais_ Uma An/303/241lise Arquitet/303/264nica para a Realiza/303/247/303/243o do Ciclo de Vida do Projeto SADDLE.md" +91 -0
  204. package/docs/talks9/Mapeamento da Engrenagem Computacional_ Uma Arquitetura para Execu/303/247/303/243o Isolada e Persist/303/252ncia em Ambientes Distribu/303/255dos.md" +116 -0
  205. package/docs/talks9/O Cen/303/241rio Pr/303/241tico do SADDLE_ Uma An/303/241lise de Viabilidade e Modelo de Ciclo de Vida Integrado.md" +128 -0
  206. package/docs/talks9/README (2).md +489 -0
  207. package/docs/talks9/README.md +198 -0
  208. package/docs/talks9/Viabilidade do Saddle_ Uma An/303/241lise T/303/251cnica da Transforma/303/247/303/243o de Armazenamento Remoto em Mem/303/263ria Computacional.md" +80 -0
  209. package/docs/talks9/conversa.txt +544 -0
  210. package/docs/talks9/other (2).md +39 -0
  211. package/docs/talks9/other.md +57 -0
  212. package/docs/talks9/outro.txt +24 -0
  213. package/extension/README.md +3 -3
  214. package/extension/serviceworker.js +63 -4
  215. package/extension/worker.js +3 -1
  216. package/package.json +12 -2
  217. package/packager/manifest.js +1 -1
  218. package/scrape/agent.ts +122 -0
  219. package/scrape/batch.ts +79 -0
  220. package/scrape/biome.json +76 -0
  221. package/scrape/browser.ts +222 -0
  222. package/scrape/cache.ts +84 -0
  223. package/scrape/chunking.ts +193 -0
  224. package/scrape/cli.ts +105 -0
  225. package/scrape/crawler.ts +115 -0
  226. package/scrape/dev-server.ts +94 -0
  227. package/scrape/errors.ts +132 -0
  228. package/scrape/events.ts +26 -0
  229. package/scrape/extract.ts +165 -0
  230. package/scrape/fetch.ts +105 -0
  231. package/scrape/formats.ts +85 -0
  232. package/scrape/headers.ts +71 -0
  233. package/scrape/index.ts +92 -0
  234. package/scrape/jsdom.d.ts +6 -0
  235. package/scrape/llms-txt.ts +84 -0
  236. package/scrape/middleware.ts +90 -0
  237. package/scrape/package-lock.json +9397 -0
  238. package/scrape/package.json +1420 -0
  239. package/scrape/pool.ts +95 -0
  240. package/scrape/port.ts +18 -0
  241. package/scrape/proxy.ts +103 -0
  242. package/scrape/rate-limiter.ts +95 -0
  243. package/scrape/renderer.ts +194 -0
  244. package/scrape/retry.ts +64 -0
  245. package/scrape/robots.ts +137 -0
  246. package/scrape/scrape.ts +123 -0
  247. package/scrape/serialize.ts +310 -0
  248. package/scrape/server.ts +137 -0
  249. package/scrape/session.ts +109 -0
  250. package/scrape/sitemap.ts +131 -0
  251. package/scrape/tokens.ts +45 -0
  252. package/scrape/tsconfig.json +28 -0
  253. package/scrape/types.ts +214 -0
  254. package/scrape/utils.ts +77 -0
  255. package/scrape/vite.config.ts +55 -0
  256. package/scrape/vitest.config.ts +17 -0
  257. package/workflow/templates.js +4 -4
@@ -0,0 +1,1350 @@
1
+ # Pesquisa: Zod Schema Validation para TypeScript (2026)
2
+
3
+ ## Sumário
4
+
5
+ 1. [Zod v4: O que mudou](#1-zod-v4-o-que-mudou)
6
+ 2. [Runtime Validation of Scraping Options](#2-runtime-validation-of-scraping-options)
7
+ 3. [Creating Zod Schemas for Core Types](#3-creating-zod-schemas-for-core-types)
8
+ 4. [API Input Validation Middleware](#4-api-input-validation-middleware)
9
+ 5. [CLI Validation with z.coerce](#5-cli-validation-with-zcoerce)
10
+ 6. [Deriving TypeScript Types](#6-deriving-typescript-types)
11
+ 7. [Best Practices](#7-best-practices)
12
+ 8. [Response Validation Pattern](#8-response-validation-pattern)
13
+ 9. [JSON Schema Generation](#9-json-schema-generation)
14
+ 10. [Complete Code Examples](#10-complete-code-examples)
15
+
16
+ ---
17
+
18
+ ## 1. Zod v4: O que mudou
19
+
20
+ ### Performance Improvements
21
+
22
+ Zod v4 (released 2025-2026) brings major improvements:
23
+
24
+ | Metric | Zod v3 | Zod v4 | Improvement |
25
+ |--------|--------|--------|-------------|
26
+ | Parse speed | ~1x | ~14x | 14x faster |
27
+ | Bundle size | ~60KB | ~26KB | 2.3x smaller |
28
+ | Type inference | Good | Excellent | Better DX |
29
+ | Error messages | Basic | Rich | More helpful |
30
+
31
+ ### Key New Features
32
+
33
+ ```typescript
34
+ import { z } from 'zod/v4';
35
+
36
+ // 1. z.strictObject — rejects unknown keys at runtime
37
+ const strictConfig = z.strictObject({
38
+ url: z.string().url(),
39
+ maxRetries: z.number().int().positive(),
40
+ // Unknown keys throw ZodError
41
+ });
42
+
43
+ // 2. .prefault() — default values computed lazily
44
+ const options = z.object({
45
+ concurrency: z.number().default(5),
46
+ timeout: z.number().default(30_000),
47
+ userAgent: z.string().prefault(() => `Bot/${Math.random().toString(36)}`),
48
+ });
49
+
50
+ // 3. z.interface() — proper interface-like semantics
51
+ const IScrapeResult = z.interface({
52
+ url: z.string(),
53
+ title: z.string(),
54
+ links: z.array(z.string()),
55
+ });
56
+
57
+ // 4. Improved error messages
58
+ const schema = z.string().email();
59
+ schema.parse('not-an-email');
60
+ // v3: "Invalid email"
61
+ // v4: "Invalid email: expected string matching email format, received 'not-an-email'"
62
+
63
+ // 5. Pattern matching
64
+ const result = z.union([z.string(), z.number()]).safeParse(true);
65
+ // v4 gives better error: "Invalid input: expected string | number, received boolean"
66
+ ```
67
+
68
+ ### Migration from v3 to v4
69
+
70
+ ```typescript
71
+ // v3
72
+ import { z } from 'zod';
73
+ const schema = z.object({ name: z.string() });
74
+
75
+ // v4 (drop-in with import path change)
76
+ import { z } from 'zod/v4';
77
+ const schema = z.object({ name: z.string() });
78
+
79
+ // For backwards compatibility
80
+ import { z } from 'zod';
81
+ // Zod v4 is backwards compatible with v3 API
82
+ // But v4 has new features only available with 'zod/v4'
83
+ ```
84
+
85
+ ---
86
+
87
+ ## 2. Runtime Validation of Scraping Options
88
+
89
+ ### Why Runtime Validation?
90
+
91
+ TypeScript types are erased at compile time. If your scraper receives options from:
92
+ - CLI arguments (strings)
93
+ - API requests (JSON)
94
+ - Config files (YAML/JSON)
95
+ - Environment variables (strings)
96
+
97
+ ...you need runtime validation to ensure they match your expected types.
98
+
99
+ ### Core Scraping Options Schema
100
+
101
+ ```typescript
102
+ import { z } from 'zod';
103
+
104
+ // Enum-like unions
105
+ const HttpMethod = z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']);
106
+ const ProxyType = z.enum(['http', 'https', 'socks5']);
107
+ const BrowserType = z.enum(['chromium', 'firefox', 'webkit']);
108
+
109
+ // URL with custom validation
110
+ const SafeUrl = z.string()
111
+ .url()
112
+ .refine(
113
+ (url) => {
114
+ try {
115
+ const parsed = new URL(url);
116
+ return ['http:', 'https:'].includes(parsed.protocol);
117
+ } catch {
118
+ return false;
119
+ }
120
+ },
121
+ { message: 'URL must use http or https protocol' }
122
+ );
123
+
124
+ // Proxy configuration
125
+ const ProxyConfig = z.object({
126
+ url: z.string().url(),
127
+ username: z.string().optional(),
128
+ password: z.string().optional(),
129
+ type: ProxyType.default('http'),
130
+ }).strict();
131
+
132
+ // Header map
133
+ const Headers = z.record(z.string(), z.string()).default({
134
+ 'Accept': 'text/html,application/xhtml+xml',
135
+ 'Accept-Language': 'en-US,en;q=0.9',
136
+ });
137
+
138
+ // Selector with timeout
139
+ const SelectorConfig = z.object({
140
+ selector: z.string().min(1),
141
+ timeout: z.number().int().positive().default(5000),
142
+ optional: z.boolean().default(false),
143
+ });
144
+
145
+ // Main scrape options — the most important schema
146
+ const ScrapeOptions = z.object({
147
+ // Target
148
+ url: SafeUrl,
149
+ method: HttpMethod.default('GET'),
150
+ headers: Headers,
151
+
152
+ // Browser settings
153
+ browser: BrowserType.default('chromium'),
154
+ headless: z.boolean().default(true),
155
+ viewport: z.object({
156
+ width: z.number().int().min(320).max(7680).default(1920),
157
+ height: z.number().int().min(240).max(4320).default(1080),
158
+ }).default({}),
159
+
160
+ // Timing
161
+ timeout: z.number().int().positive().default(30_000),
162
+ waitForSelector: SelectorConfig.optional(),
163
+ waitAfterLoad: z.number().int().min(0).default(0),
164
+
165
+ // Retry
166
+ maxRetries: z.number().int().min(0).max(10).default(3),
167
+ retryDelay: z.number().int().min(0).default(1000),
168
+ retryBackoff: z.enum(['linear', 'exponential']).default('exponential'),
169
+
170
+ // Proxy
171
+ proxy: ProxyConfig.optional(),
172
+
173
+ // Extraction
174
+ selectors: z.record(z.string(), SelectorConfig).optional(),
175
+ evaluatePage: z.string().optional(), // Serialized function
176
+
177
+ // Session
178
+ sessionId: z.string().uuid().optional(),
179
+ cookies: z.array(z.object({
180
+ name: z.string(),
181
+ value: z.string(),
182
+ domain: z.string().optional(),
183
+ path: z.string().default('/'),
184
+ })).optional(),
185
+
186
+ // Rate limiting
187
+ maxConcurrency: z.number().int().min(1).max(100).default(5),
188
+ requestsPerSecond: z.number().positive().optional(),
189
+ delayBetweenRequests: z.number().int().min(0).optional(),
190
+
191
+ // Output
192
+ format: z.enum(['html', 'text', 'json', 'screenshot']).default('html'),
193
+ screenshotFullPage: z.boolean().default(false),
194
+ }).strict();
195
+
196
+ // Cross-field refinements
197
+ const ScrapeOptionsWithRefinements = ScrapeOptions.refine(
198
+ (opts) => {
199
+ // If using screenshot, must be in headless mode or have specific format
200
+ if (opts.screenshotFullPage && opts.format !== 'screenshot') {
201
+ return false;
202
+ }
203
+ return true;
204
+ },
205
+ {
206
+ message: 'screenshotFullPage requires format="screenshot"',
207
+ path: ['screenshotFullPage'],
208
+ }
209
+ ).refine(
210
+ (opts) => {
211
+ // requestsPerSecond and delayBetweenRequests are mutually exclusive
212
+ if (opts.requestsPerSecond && opts.delayBetweenRequests) {
213
+ return false;
214
+ }
215
+ return true;
216
+ },
217
+ {
218
+ message: 'Cannot set both requestsPerSecond and delayBetweenRequests',
219
+ path: ['requestsPerSecond'],
220
+ }
221
+ );
222
+ ```
223
+
224
+ ### Browser Agent Config Schema
225
+
226
+ ```typescript
227
+ const BrowserAgentConfig = z.object({
228
+ // Browser management
229
+ browserPool: z.object({
230
+ minBrowsers: z.number().int().min(1).default(1),
231
+ maxBrowsers: z.number().int().min(1).max(20).default(5),
232
+ maxPagesPerBrowser: z.number().int().min(1).max(50).default(10),
233
+ idleTimeout: z.number().int().min(1000).default(60_000),
234
+ launchTimeout: z.number().int().min(1000).default(30_000),
235
+ }).default({}),
236
+
237
+ // Session management
238
+ sessions: z.object({
239
+ maxSessions: z.number().int().min(1).default(100),
240
+ sessionTimeout: z.number().int().min(60_000).default(3_600_000),
241
+ rotateOnBlock: z.boolean().default(true),
242
+ cookiePersistence: z.boolean().default(true),
243
+ }).default({}),
244
+
245
+ // Anti-detection
246
+ stealth: z.object({
247
+ enabled: z.boolean().default(true),
248
+ randomizeViewport: z.boolean().default(true),
249
+ randomizeTiming: z.boolean().default(true),
250
+ minDelay: z.number().int().min(0).default(500),
251
+ maxDelay: z.number().int().min(0).default(2000),
252
+ humanizeMouse: z.boolean().default(true),
253
+ }).default({}),
254
+
255
+ // Plugin system
256
+ plugins: z.array(z.object({
257
+ name: z.string(),
258
+ enabled: z.boolean().default(true),
259
+ config: z.record(z.unknown()).default({}),
260
+ })).default([]),
261
+
262
+ // Error handling
263
+ errorHandling: z.object({
264
+ maxConsecutiveErrors: z.number().int().min(1).default(5),
265
+ circuitBreakerThreshold: z.number().min(0).max(1).default(0.5),
266
+ circuitBreakerResetTimeout: z.number().int().min(1000).default(60_000),
267
+ onCriticalError: z.enum(['stop', 'skip', 'restart']).default('stop'),
268
+ }).default({}),
269
+
270
+ // Logging
271
+ logging: z.object({
272
+ level: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
273
+ requestLogging: z.boolean().default(true),
274
+ errorLogging: z.boolean().default(true),
275
+ }).default({}),
276
+ }).strict();
277
+ ```
278
+
279
+ ---
280
+
281
+ ## 3. Creating Zod Schemas for Core Types
282
+
283
+ ### Data Extraction Schema
284
+
285
+ ```typescript
286
+ // Schema for scraped data
287
+ const ScrapedItem = z.object({
288
+ url: z.string().url(),
289
+ title: z.string(),
290
+ description: z.string().optional(),
291
+ content: z.string().optional(),
292
+ links: z.array(z.string().url()).default([]),
293
+ images: z.array(z.object({
294
+ src: z.string().url(),
295
+ alt: z.string().default(''),
296
+ })).default([]),
297
+ metadata: z.record(z.string(), z.unknown()).default({}),
298
+ scrapedAt: z.string().datetime().default(() => new Date().toISOString()),
299
+ });
300
+
301
+ // Schema for structured data extraction
302
+ const ExtractionRule = z.object({
303
+ name: z.string().min(1),
304
+ selector: z.string().min(1),
305
+ type: z.enum(['text', 'html', 'attribute', 'list', 'count']),
306
+ attribute: z.string().optional(), // For type='attribute'
307
+ multiple: z.boolean().default(false),
308
+ required: z.boolean().default(true),
309
+ transform: z.enum(['string', 'number', 'boolean', 'date', 'json']).optional(),
310
+ default: z.unknown().optional(),
311
+ }).refine(
312
+ (rule) => rule.type === 'attribute' ? !!rule.attribute : true,
313
+ { message: 'attribute name required when type="attribute"', path: ['attribute'] }
314
+ );
315
+
316
+ const ExtractionConfig = z.object({
317
+ items: z.string().min(1), // Container selector
318
+ rules: z.array(ExtractionRule).min(1),
319
+ pagination: z.object({
320
+ nextSelector: z.string().min(1),
321
+ maxPages: z.number().int().min(1).default(10),
322
+ waitForLoad: z.number().int().min(0).default(2000),
323
+ }).optional(),
324
+ }).strict();
325
+ ```
326
+
327
+ ### Queue and Job Schema
328
+
329
+ ```typescript
330
+ const JobPriority = z.enum(['critical', 'high', 'normal', 'low']);
331
+
332
+ const ScrapeJob = z.object({
333
+ id: z.string().uuid().default(() => crypto.randomUUID()),
334
+ url: z.string().url(),
335
+ priority: JobPriority.default('normal'),
336
+ options: ScrapeOptions.partial().default({}),
337
+ metadata: z.record(z.unknown()).default({}),
338
+ createdAt: z.string().datetime().default(() => new Date().toISOString()),
339
+ attempts: z.number().int().min(0).default(0),
340
+ maxAttempts: z.number().int().min(1).default(3),
341
+ status: z.enum(['pending', 'running', 'completed', 'failed', 'cancelled']).default('pending'),
342
+ }).strict();
343
+
344
+ const BatchConfig = z.object({
345
+ jobs: z.array(ScrapeJob).min(1).max(10_000),
346
+ concurrency: z.number().int().min(1).max(100).default(10),
347
+ rateLimit: z.object({
348
+ requestsPerSecond: z.number().positive(),
349
+ burstSize: z.number().int().positive().default(5),
350
+ }).optional(),
351
+ deadline: z.string().datetime().optional(),
352
+ onProgress: z.function().optional(), // Can't validate functions with Zod
353
+ }).strict();
354
+ ```
355
+
356
+ ---
357
+
358
+ ## 4. API Input Validation Middleware
359
+
360
+ ### Express/Hono/Fastify Pattern
361
+
362
+ ```typescript
363
+ import { z } from 'zod';
364
+ import type { ZodSchema, ZodError } from 'zod';
365
+ import type { Request, Response, NextFunction } from 'express';
366
+
367
+ // Generic validation middleware
368
+ function validate<T extends ZodSchema>(
369
+ schema: T,
370
+ source: 'body' | 'query' | 'params' = 'body'
371
+ ) {
372
+ return (req: Request, res: Response, next: NextFunction) => {
373
+ const result = schema.safeParse(req[source]);
374
+
375
+ if (!result.success) {
376
+ const errors = formatZodError(result.error);
377
+ return res.status(400).json({
378
+ error: 'Validation failed',
379
+ details: errors,
380
+ });
381
+ }
382
+
383
+ // Replace with validated/transformed data
384
+ (req as any)[source] = result.data;
385
+ next();
386
+ };
387
+ }
388
+
389
+ // Format ZodError into a readable structure
390
+ function formatZodError(error: ZodError): Array<{
391
+ path: string;
392
+ message: string;
393
+ code: string;
394
+ }> {
395
+ return error.issues.map((issue) => ({
396
+ path: issue.path.join('.'),
397
+ message: issue.message,
398
+ code: issue.code,
399
+ }));
400
+ }
401
+
402
+ // Usage with Express
403
+ import express from 'express';
404
+
405
+ const app = express();
406
+ app.use(express.json());
407
+
408
+ // POST /api/scrape — validates body
409
+ app.post(
410
+ '/api/scrape',
411
+ validate(ScrapeOptions),
412
+ async (req, res) => {
413
+ // req.body is now typed as ScrapeOptions (validated)
414
+ const result = await scrape(req.body);
415
+ res.json(result);
416
+ }
417
+ );
418
+
419
+ // GET /api/jobs/:id — validates params
420
+ app.get(
421
+ '/api/jobs/:id',
422
+ validate(z.object({ id: z.string().uuid() }), 'params'),
423
+ async (req, res) => {
424
+ // req.params.id is validated as UUID
425
+ const job = await getJob(req.params.id);
426
+ res.json(job);
427
+ }
428
+ );
429
+
430
+ // GET /api/jobs — validates query
431
+ app.get(
432
+ '/api/jobs',
433
+ validate(
434
+ z.object({
435
+ page: z.coerce.number().int().min(1).default(1),
436
+ limit: z.coerce.number().int().min(1).max(100).default(20),
437
+ status: z.enum(['pending', 'running', 'completed', 'failed']).optional(),
438
+ }),
439
+ 'query'
440
+ ),
441
+ async (req, res) => {
442
+ // req.query is validated and coerced
443
+ const jobs = await listJobs(req.query);
444
+ res.json(jobs);
445
+ }
446
+ );
447
+ ```
448
+
449
+ ### Hono-Specific Pattern
450
+
451
+ ```typescript
452
+ import { Hono } from 'hono';
453
+ import { zValidator } from '@hono/zod-validator';
454
+
455
+ const app = new Hono();
456
+
457
+ // Hono has built-in Zod validator
458
+ app.post(
459
+ '/api/scrape',
460
+ zValidator('json', ScrapeOptions, (result, c) => {
461
+ if (!result.success) {
462
+ return c.json({ error: result.error.issues }, 400);
463
+ }
464
+ }),
465
+ async (c) => {
466
+ const body = c.req.valid('json');
467
+ // body is fully typed and validated
468
+ const result = await scrape(body);
469
+ return c.json(result);
470
+ }
471
+ );
472
+ ```
473
+
474
+ ### Fastify Pattern
475
+
476
+ ```typescript
477
+ import Fastify from 'fastify';
478
+ import { Type } from '@sinclair/typebox';
479
+ import { Value } from '@sinclair/typebox';
480
+
481
+ // Or use Zod with fastify-type-provider-zod
482
+ import { ZodTypeProvider } from 'fastify-type-provider-zod';
483
+ import { z } from 'zod';
484
+
485
+ const fastify = Fastify().withTypeProvider<ZodTypeProvider>();
486
+
487
+ fastify.post('/api/scrape', {
488
+ schema: {
489
+ body: ScrapeOptions,
490
+ response: {
491
+ 200: z.object({
492
+ success: z.boolean(),
493
+ data: ScrapedItem,
494
+ }),
495
+ },
496
+ },
497
+ }, async (request, reply) => {
498
+ // request.body is typed
499
+ const result = await scrape(request.body);
500
+ return { success: true, data: result };
501
+ });
502
+ ```
503
+
504
+ ---
505
+
506
+ ## 5. CLI Validation with z.coerce
507
+
508
+ ### The Problem
509
+
510
+ CLI arguments are always strings. `z.coerce` converts them to the correct type during validation.
511
+
512
+ ```typescript
513
+ import { z } from 'zod';
514
+
515
+ // CLI argument schema
516
+ const CliOptions = z.object({
517
+ // String arguments (no coercion needed)
518
+ url: z.string().url('Invalid URL'),
519
+ output: z.string().default('./output'),
520
+
521
+ // Coerced number arguments
522
+ concurrency: z.coerce.number()
523
+ .int()
524
+ .min(1, 'Concurrency must be at least 1')
525
+ .max(100, 'Concurrency cannot exceed 100')
526
+ .default(5),
527
+
528
+ timeout: z.coerce.number()
529
+ .int()
530
+ .min(1000, 'Timeout must be at least 1000ms')
531
+ .default(30_000),
532
+
533
+ maxRetries: z.coerce.number()
534
+ .int()
535
+ .min(0)
536
+ .max(10)
537
+ .default(3),
538
+
539
+ // Coerced boolean arguments
540
+ headless: z.coerce.boolean().default(true),
541
+ verbose: z.coerce.boolean().default(false),
542
+ followRedirects: z.coerce.boolean().default(true),
543
+
544
+ // Enum arguments
545
+ browser: z.enum(['chromium', 'firefox', 'webkit']).default('chromium'),
546
+ format: z.enum(['json', 'csv', 'html']).default('json'),
547
+
548
+ // Date coercion
549
+ since: z.coerce.date().optional(),
550
+
551
+ // Array arguments (comma-separated)
552
+ urls: z.string().transform((val) => val.split(',').map(u => u.trim())),
553
+ }).strict();
554
+
555
+ // Parse CLI args
556
+ function parseCliArgs(argv: string[]): z.infer<typeof CliOptions> {
557
+ // Simple arg parser (use commander/yargs in production)
558
+ const args: Record<string, string> = {};
559
+
560
+ for (let i = 2; i < argv.length; i++) {
561
+ const arg = argv[i];
562
+ if (arg.startsWith('--')) {
563
+ const [key, value] = arg.slice(2).split('=');
564
+ args[key] = value ?? argv[++i] ?? 'true';
565
+ }
566
+ }
567
+
568
+ const result = CliOptions.safeParse(args);
569
+
570
+ if (!result.success) {
571
+ console.error('Invalid arguments:');
572
+ for (const issue of result.error.issues) {
573
+ console.error(` --${issue.path.join('.')}: ${issue.message}`);
574
+ }
575
+ process.exit(1);
576
+ }
577
+
578
+ return result.data;
579
+ }
580
+
581
+ // Usage
582
+ const options = parseCliArgs(process.argv);
583
+ console.log(`Scraping ${options.url} with ${options.concurrency} concurrent requests`);
584
+ ```
585
+
586
+ ### Commander.js Integration
587
+
588
+ ```typescript
589
+ import { Command } from 'commander';
590
+ import { z } from 'zod';
591
+
592
+ const program = new Command();
593
+
594
+ const scrapeSchema = z.object({
595
+ url: z.string().url(),
596
+ concurrency: z.coerce.number().int().min(1).max(100).default(5),
597
+ timeout: z.coerce.number().int().min(1000).default(30_000),
598
+ headless: z.coerce.boolean().default(true),
599
+ browser: z.enum(['chromium', 'firefox', 'webkit']).default('chromium'),
600
+ output: z.string().default('./output'),
601
+ verbose: z.coerce.boolean().default(false),
602
+ });
603
+
604
+ program
605
+ .name('scraper')
606
+ .description('Web scraper CLI')
607
+ .version('1.0.0')
608
+ .argument('<url>', 'URL to scrape')
609
+ .option('-c, --concurrency <number>', 'Concurrent requests', '5')
610
+ .option('-t, --timeout <number>', 'Request timeout in ms', '30000')
611
+ .option('--no-headless', 'Run browser in headed mode')
612
+ .option('-b, --browser <type>', 'Browser type', 'chromium')
613
+ .option('-o, --output <dir>', 'Output directory', './output')
614
+ .option('-v, --verbose', 'Verbose output')
615
+ .action(async (url, opts) => {
616
+ const result = scrapeSchema.safeParse({ url, ...opts });
617
+
618
+ if (!result.success) {
619
+ console.error('Validation error:');
620
+ result.error.issues.forEach(i => {
621
+ console.error(` ${i.path.join('.')}: ${i.message}`);
622
+ });
623
+ process.exit(1);
624
+ }
625
+
626
+ await runScraper(result.data);
627
+ });
628
+
629
+ program.parse();
630
+ ```
631
+
632
+ ---
633
+
634
+ ## 6. Deriving TypeScript Types
635
+
636
+ ### The z.infer Pattern
637
+
638
+ Zod schemas are the source of truth. TypeScript types are derived from them — never the other way around.
639
+
640
+ ```typescript
641
+ import { z } from 'zod';
642
+
643
+ // Define the schema
644
+ const ScrapeOptions = z.object({
645
+ url: z.string().url(),
646
+ concurrency: z.number().int().default(5),
647
+ headless: z.boolean().default(true),
648
+ proxy: z.object({
649
+ url: z.string().url(),
650
+ username: z.string().optional(),
651
+ }).optional(),
652
+ });
653
+
654
+ // Derive the TypeScript type
655
+ type ScrapeOptions = z.infer<typeof ScrapeOptions>;
656
+ // {
657
+ // url: string;
658
+ // concurrency: number;
659
+ // headless: boolean;
660
+ // proxy?: { url: string; username?: string } | undefined;
661
+ // }
662
+
663
+ // For input (before defaults are applied)
664
+ type ScrapeOptionsInput = z.input<typeof ScrapeOptions>;
665
+ // { url: string; concurrency?: number; headless?: boolean; proxy?: ... }
666
+
667
+ // For output (after defaults, transforms, refinements)
668
+ type ScrapeOptionsOutput = z.output<typeof ScrapeOptions>;
669
+ // { url: string; concurrency: number; headless: boolean; proxy?: ... }
670
+ ```
671
+
672
+ ### Extracting Types from Complex Schemas
673
+
674
+ ```typescript
675
+ const ScrapedItem = z.object({
676
+ url: z.string().url(),
677
+ title: z.string(),
678
+ data: z.record(z.string(), z.unknown()),
679
+ tags: z.array(z.string()),
680
+ metadata: z.object({
681
+ scrapedAt: z.string().datetime(),
682
+ duration: z.number(),
683
+ status: z.enum(['success', 'partial', 'failed']),
684
+ }),
685
+ });
686
+
687
+ // Full type
688
+ type ScrapedItem = z.infer<typeof ScrapedItem>;
689
+
690
+ // Partial type (all optional)
691
+ const PartialScrapedItem = ScrapedItem.partial();
692
+ type PartialScrapedItem = z.infer<typeof PartialScrapedItem>;
693
+
694
+ // Pick specific fields
695
+ const ScrapedItemSummary = ScrapedItem.pick({
696
+ url: true,
697
+ title: true,
698
+ metadata: true,
699
+ });
700
+ type ScrapedItemSummary = z.infer<typeof ScrapedItemSummary>;
701
+
702
+ // Omit specific fields
703
+ const ScrapedItemData = ScrapedItem.omit({ metadata: true });
704
+ type ScrapedItemData = z.infer<typeof ScrapedItemData>;
705
+
706
+ // Extend with additional fields
707
+ const ExtendedItem = ScrapedItem.extend({
708
+ screenshot: z.string().optional(),
709
+ pdf: z.string().optional(),
710
+ });
711
+ type ExtendedItem = z.infer<typeof ExtendedItem>;
712
+ ```
713
+
714
+ ### Utility Types from Zod
715
+
716
+ ```typescript
717
+ // Make specific fields required
718
+ const RequiredConfig = BrowserAgentConfig.required({
719
+ browserPool: true,
720
+ sessions: true,
721
+ });
722
+
723
+ // Deep partial (recursive)
724
+ const DeepPartialScrapeOptions = ScrapeOptions.deepPartial();
725
+ type DeepPartialScrapeOptions = z.infer<typeof DeepPartialScrapeOptions>;
726
+
727
+ // Brand types for nominal typing
728
+ const UserId = z.string().uuid().brand<'UserId'>();
729
+ type UserId = z.infer<typeof UserId>;
730
+
731
+ function getUser(id: UserId) { /* ... */ }
732
+
733
+ // ✅ Works
734
+ const id = UserId.parse('550e8400-e29b-41d4-a716-446655440000');
735
+ getUser(id);
736
+
737
+ // ❌ Compile error — can't pass random string
738
+ getUser('550e8400-e29b-41d4-a716-446655440000');
739
+ ```
740
+
741
+ ---
742
+
743
+ ## 7. Best Practices
744
+
745
+ ### Validate at Boundaries, Trust Internally
746
+
747
+ ```typescript
748
+ // ❌ BAD: Validating everywhere
749
+ function processItem(item: ScrapedItem) {
750
+ const validated = ScrapedItem.parse(item); // Unnecessary!
751
+ return validated.title;
752
+ }
753
+
754
+ // ✅ GOOD: Validate once at the boundary
755
+ async function scrapeAndProcess(url: string): Promise<ProcessedItem> {
756
+ // Boundary: incoming data from external source
757
+ const html = await fetch(url).then(r => r.text());
758
+ const raw = extractData(html); // Returns unknown
759
+
760
+ // Validate once
761
+ const item = ScrapedItem.parse(raw);
762
+
763
+ // Trust internally — no more validation needed
764
+ return processItem(item);
765
+ }
766
+
767
+ function processItem(item: ScrapedItem): ProcessedItem {
768
+ // item is guaranteed to be ScrapedItem
769
+ return { title: item.title.toUpperCase() };
770
+ }
771
+ ```
772
+
773
+ ### Use safeParse at Boundaries
774
+
775
+ ```typescript
776
+ // For operations that shouldn't throw
777
+ function tryParseScrapeOptions(input: unknown): ScrapeOptions | null {
778
+ const result = ScrapeOptions.safeParse(input);
779
+ if (result.success) {
780
+ return result.data;
781
+ }
782
+ console.error('Invalid options:', result.error.format());
783
+ return null;
784
+ }
785
+
786
+ // For API responses
787
+ async function fetchAndValidate<T extends z.ZodSchema>(
788
+ url: string,
789
+ schema: T
790
+ ): Promise<z.infer<T>> {
791
+ const res = await fetch(url);
792
+ const json = await res.json();
793
+
794
+ const result = schema.safeParse(json);
795
+ if (!result.success) {
796
+ throw new ScraperError('Invalid API response', {
797
+ code: 'RESPONSE_VALIDATION_FAILED',
798
+ metadata: { url, errors: result.error.issues },
799
+ });
800
+ }
801
+
802
+ return result.data;
803
+ }
804
+ ```
805
+
806
+ ### Compose Schemas
807
+
808
+ ```typescript
809
+ // Build complex schemas from simple ones
810
+ const BaseEntity = z.object({
811
+ id: z.string().uuid(),
812
+ createdAt: z.string().datetime(),
813
+ updatedAt: z.string().datetime(),
814
+ });
815
+
816
+ const ScrapedPage = BaseEntity.extend({
817
+ url: z.string().url(),
818
+ title: z.string(),
819
+ content: z.string(),
820
+ links: z.array(z.string().url()),
821
+ });
822
+
823
+ const ScrapedProduct = BaseEntity.extend({
824
+ url: z.string().url(),
825
+ name: z.string(),
826
+ price: z.number().positive(),
827
+ inStock: z.boolean(),
828
+ images: z.array(z.string().url()),
829
+ });
830
+
831
+ // Discriminated union for different scraped types
832
+ const ScrapedEntity = z.discriminatedUnion('type', [
833
+ ScrapedPage.extend({ type: z.literal('page') }),
834
+ ScrapedProduct.extend({ type: z.literal('product') }),
835
+ ]);
836
+ ```
837
+
838
+ ---
839
+
840
+ ## 8. Response Validation Pattern
841
+
842
+ ### Validating External API Responses
843
+
844
+ ```typescript
845
+ import { z } from 'zod';
846
+
847
+ // Schema for a specific API
848
+ const ExternalApiResponse = z.object({
849
+ status: z.enum(['ok', 'error']),
850
+ data: z.object({
851
+ results: z.array(z.object({
852
+ id: z.number(),
853
+ title: z.string(),
854
+ body: z.string(),
855
+ })),
856
+ total: z.number(),
857
+ page: z.number(),
858
+ }).optional(),
859
+ error: z.string().optional(),
860
+ }).refine(
861
+ (res) => res.status === 'ok' ? !!res.data : !!res.error,
862
+ { message: 'Response must have data when status is ok, or error when status is error' }
863
+ );
864
+
865
+ async function fetchExternalData(query: string) {
866
+ const res = await fetch(`https://api.example.com/search?q=${query}`);
867
+ const json = await res.json();
868
+
869
+ const result = ExternalApiResponse.safeParse(json);
870
+
871
+ if (!result.success) {
872
+ // Log the raw response for debugging
873
+ console.error('Unexpected API response format:', json);
874
+ throw new ScraperError('External API returned unexpected format', {
875
+ code: 'RESPONSE_VALIDATION_FAILED',
876
+ metadata: {
877
+ issues: result.error.issues,
878
+ rawResponse: json,
879
+ },
880
+ });
881
+ }
882
+
883
+ if (result.data.status === 'error') {
884
+ throw new ScraperError(`External API error: ${result.data.error}`, {
885
+ code: 'EXTERNAL_API_ERROR',
886
+ });
887
+ }
888
+
889
+ return result.data.data!;
890
+ }
891
+ ```
892
+
893
+ ### Validating Scraped HTML Structure
894
+
895
+ ```typescript
896
+ // After extracting data from HTML, validate the structure
897
+ const HtmlExtractedData = z.object({
898
+ title: z.string().min(1),
899
+ price: z.string().transform((val) => {
900
+ const num = parseFloat(val.replace(/[^0-9.]/g, ''));
901
+ if (isNaN(num)) throw new Error(`Invalid price: ${val}`);
902
+ return num;
903
+ }),
904
+ rating: z.string().transform((val) => {
905
+ const num = parseFloat(val);
906
+ if (num < 0 || num > 5) throw new Error(`Rating out of range: ${val}`);
907
+ return num;
908
+ }),
909
+ reviewCount: z.string().transform((val) => {
910
+ const num = parseInt(val.replace(/[^0-9]/g, ''), 10);
911
+ if (isNaN(num)) throw new Error(`Invalid review count: ${val}`);
912
+ return num;
913
+ }),
914
+ images: z.array(z.string().url()),
915
+ inStock: z.string().transform((val) => {
916
+ const lower = val.toLowerCase();
917
+ return lower.includes('in stock') || lower.includes('available');
918
+ }),
919
+ });
920
+
921
+ // Transform and validate in one step
922
+ function parseProductPage(html: string): z.infer<typeof HtmlExtractedData> {
923
+ const raw = extractFromHtml(html); // Returns unknown
924
+ return HtmlExtractedData.parse(raw); // Throws on invalid data
925
+ }
926
+ ```
927
+
928
+ ---
929
+
930
+ ## 9. JSON Schema Generation
931
+
932
+ ### Zod v4: z.toJSONSchema
933
+
934
+ ```typescript
935
+ import { z } from 'zod/v4';
936
+
937
+ const ScrapeOptions = z.object({
938
+ url: z.string().url(),
939
+ concurrency: z.number().int().min(1).max(100).default(5),
940
+ headless: z.boolean().default(true),
941
+ browser: z.enum(['chromium', 'firefox', 'webkit']).default('chromium'),
942
+ proxy: z.object({
943
+ url: z.string().url(),
944
+ type: z.enum(['http', 'https', 'socks5']).default('http'),
945
+ }).optional(),
946
+ selectors: z.record(z.string(), z.object({
947
+ selector: z.string(),
948
+ type: z.enum(['text', 'html', 'attribute']),
949
+ })).optional(),
950
+ });
951
+
952
+ // Generate JSON Schema
953
+ const jsonSchema = z.toJSONSchema(ScrapeOptions);
954
+
955
+ console.log(JSON.stringify(jsonSchema, null, 2));
956
+ // Output:
957
+ // {
958
+ // "type": "object",
959
+ // "properties": {
960
+ // "url": { "type": "string", "format": "uri" },
961
+ // "concurrency": { "type": "integer", "minimum": 1, "maximum": 100, "default": 5 },
962
+ // "headless": { "type": "boolean", "default": true },
963
+ // "browser": { "type": "string", "enum": ["chromium", "firefox", "webkit"], "default": "chromium" },
964
+ // "proxy": {
965
+ // "type": "object",
966
+ // "properties": {
967
+ // "url": { "type": "string", "format": "uri" },
968
+ // "type": { "type": "string", "enum": ["http", "https", "socks5"], "default": "http" }
969
+ // },
970
+ // "required": ["url"]
971
+ // },
972
+ // "selectors": {
973
+ // "type": "object",
974
+ // "additionalProperties": {
975
+ // "type": "object",
976
+ // "properties": {
977
+ // "selector": { "type": "string" },
978
+ // "type": { "type": "string", "enum": ["text", "html", "attribute"] }
979
+ // },
980
+ // "required": ["selector", "type"]
981
+ // }
982
+ // }
983
+ // },
984
+ // "required": ["url"],
985
+ // "additionalProperties": false
986
+ // }
987
+ ```
988
+
989
+ ### Use Cases for JSON Schema
990
+
991
+ ```typescript
992
+ // 1. Generate OpenAPI docs
993
+ import { z } from 'zod/v4';
994
+
995
+ const apiSchema = z.object({
996
+ body: ScrapeOptions,
997
+ response: z.object({
998
+ success: z.boolean(),
999
+ data: ScrapedItem,
1000
+ }),
1001
+ });
1002
+
1003
+ const jsonSchema = z.toJSONSchema(apiSchema);
1004
+ // Use with swagger-ui or redoc
1005
+
1006
+ // 2. Generate VS Code settings schema
1007
+ const VsCodeSettings = z.object({
1008
+ 'scraper.defaultBrowser': z.enum(['chromium', 'firefox', 'webkit']),
1009
+ 'scraper.concurrency': z.number().int().min(1).max(100),
1010
+ 'scraper.outputDir': z.string(),
1011
+ });
1012
+
1013
+ const vscodeSchema = z.toJSONSchema(VsCodeSettings);
1014
+ // Write to .vscode/settings.schema.json
1015
+
1016
+ // 3. Validate config files at runtime
1017
+ const configSchema = z.object({
1018
+ targets: z.array(z.object({
1019
+ url: z.string().url(),
1020
+ schedule: z.string(), // cron expression
1021
+ })),
1022
+ global: BrowserAgentConfig,
1023
+ });
1024
+
1025
+ // Load and validate config
1026
+ const configPath = path.resolve('scraper.config.json');
1027
+ const configJson = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
1028
+ const config = configSchema.parse(configJson);
1029
+
1030
+ // 4. Client-side form validation
1031
+ // Use JSON Schema to generate form fields in React/Vue
1032
+ const formSchema = z.toJSONSchema(ScrapeOptions);
1033
+ // Pass to react-jsonschema-form or similar
1034
+ ```
1035
+
1036
+ ### OpenAPI Integration
1037
+
1038
+ ```typescript
1039
+ import { z } from 'zod/v4';
1040
+ import { createDocument } from 'zod-openapi';
1041
+
1042
+ const app = z.object({
1043
+ openapi: z.literal('3.1.0'),
1044
+ info: z.object({
1045
+ title: 'Scraper API',
1046
+ version: z.string(),
1047
+ }),
1048
+ paths: z.record(z.string(), z.object({
1049
+ post: z.object({
1050
+ requestBody: z.object({
1051
+ content: z.record(z.string(), z.object({
1052
+ schema: z.toJSONSchema(ScrapeOptions),
1053
+ })),
1054
+ }),
1055
+ responses: z.record(z.string(), z.object({
1056
+ description: z.string(),
1057
+ content: z.record(z.string(), z.object({
1058
+ schema: z.toJSONSchema(ScrapedItem),
1059
+ })),
1060
+ })),
1061
+ }),
1062
+ })),
1063
+ });
1064
+
1065
+ // Generate OpenAPI spec
1066
+ const spec = app.parse({
1067
+ openapi: '3.1.0',
1068
+ info: { title: 'Scraper API', version: '1.0.0' },
1069
+ paths: {
1070
+ '/api/scrape': {
1071
+ post: {
1072
+ requestBody: {
1073
+ content: {
1074
+ 'application/json': {
1075
+ schema: z.toJSONSchema(ScrapeOptions),
1076
+ },
1077
+ },
1078
+ },
1079
+ responses: {
1080
+ '200': {
1081
+ description: 'Success',
1082
+ content: {
1083
+ 'application/json': {
1084
+ schema: z.toJSONSchema(ScrapedItem),
1085
+ },
1086
+ },
1087
+ },
1088
+ },
1089
+ },
1090
+ },
1091
+ },
1092
+ });
1093
+ ```
1094
+
1095
+ ---
1096
+
1097
+ ## 10. Complete Code Examples
1098
+
1099
+ ### Full Scraper Config Validation System
1100
+
1101
+ ```typescript
1102
+ import { z } from 'zod';
1103
+
1104
+ // ============================================
1105
+ // Schema Definitions
1106
+ // ============================================
1107
+
1108
+ const UrlSchema = z.string().url().refine(
1109
+ (url) => {
1110
+ try {
1111
+ const parsed = new URL(url);
1112
+ return ['http:', 'https:'].includes(parsed.protocol);
1113
+ } catch {
1114
+ return false;
1115
+ }
1116
+ },
1117
+ { message: 'URL must use http or https protocol' }
1118
+ );
1119
+
1120
+ const ProxySchema = z.object({
1121
+ url: UrlSchema,
1122
+ username: z.string().optional(),
1123
+ password: z.string().optional(),
1124
+ type: z.enum(['http', 'https', 'socks5']).default('http'),
1125
+ }).strict();
1126
+
1127
+ const SelectorSchema = z.object({
1128
+ selector: z.string().min(1, 'Selector cannot be empty'),
1129
+ timeout: z.number().int().min(100).max(60_000).default(5000),
1130
+ optional: z.boolean().default(false),
1131
+ waitForVisible: z.boolean().default(false),
1132
+ }).strict();
1133
+
1134
+ const ScrapeOptionsSchema = z.object({
1135
+ url: UrlSchema,
1136
+ method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']).default('GET'),
1137
+ headers: z.record(z.string(), z.string()).default({
1138
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9',
1139
+ 'Accept-Language': 'en-US,en;q=0.9',
1140
+ }),
1141
+ body: z.union([z.string(), z.record(z.unknown())]).optional(),
1142
+ browser: z.enum(['chromium', 'firefox', 'webkit']).default('chromium'),
1143
+ headless: z.boolean().default(true),
1144
+ viewport: z.object({
1145
+ width: z.number().int().min(320).max(7680).default(1920),
1146
+ height: z.number().int().min(240).max(4320).default(1080),
1147
+ }).default({}),
1148
+ timeout: z.number().int().min(1000).max(300_000).default(30_000),
1149
+ waitForSelector: SelectorSchema.optional(),
1150
+ waitAfterLoad: z.number().int().min(0).max(60_000).default(0),
1151
+ maxRetries: z.number().int().min(0).max(10).default(3),
1152
+ retryDelay: z.number().int().min(0).max(60_000).default(1000),
1153
+ retryBackoff: z.enum(['linear', 'exponential']).default('exponential'),
1154
+ proxy: ProxySchema.optional(),
1155
+ selectors: z.record(z.string(), SelectorSchema).optional(),
1156
+ sessionId: z.string().uuid().optional(),
1157
+ cookies: z.array(z.object({
1158
+ name: z.string(),
1159
+ value: z.string(),
1160
+ domain: z.string().optional(),
1161
+ path: z.string().default('/'),
1162
+ })).optional(),
1163
+ maxConcurrency: z.number().int().min(1).max(100).default(5),
1164
+ requestsPerSecond: z.number().positive().optional(),
1165
+ delayBetweenRequests: z.number().int().min(0).max(60_000).optional(),
1166
+ format: z.enum(['html', 'text', 'json', 'screenshot']).default('html'),
1167
+ screenshotFullPage: z.boolean().default(false),
1168
+ }).strict()
1169
+ .refine(
1170
+ (opts) => !(opts.requestsPerSecond && opts.delayBetweenRequests),
1171
+ { message: 'Cannot set both requestsPerSecond and delayBetweenRequests' }
1172
+ )
1173
+ .refine(
1174
+ (opts) => opts.screenshotFullPage ? opts.format === 'screenshot' : true,
1175
+ { message: 'screenshotFullPage requires format="screenshot"' }
1176
+ );
1177
+
1178
+ const BatchConfigSchema = z.object({
1179
+ urls: z.array(UrlSchema).min(1).max(10_000),
1180
+ defaultOptions: ScrapeOptionsSchema.partial().default({}),
1181
+ concurrency: z.number().int().min(1).max(100).default(10),
1182
+ rateLimit: z.object({
1183
+ requestsPerSecond: z.number().positive(),
1184
+ burstSize: z.number().int().positive().default(5),
1185
+ }).optional(),
1186
+ deadline: z.string().datetime().optional(),
1187
+ onProgress: z.function().optional(),
1188
+ }).strict();
1189
+
1190
+ const BrowserAgentConfigSchema = z.object({
1191
+ browserPool: z.object({
1192
+ minBrowsers: z.number().int().min(1).default(1),
1193
+ maxBrowsers: z.number().int().min(1).max(20).default(5),
1194
+ maxPagesPerBrowser: z.number().int().min(1).max(50).default(10),
1195
+ idleTimeout: z.number().int().min(1000).default(60_000),
1196
+ launchTimeout: z.number().int().min(1000).default(30_000),
1197
+ }).default({}),
1198
+ sessions: z.object({
1199
+ maxSessions: z.number().int().min(1).default(100),
1200
+ sessionTimeout: z.number().int().min(60_000).default(3_600_000),
1201
+ rotateOnBlock: z.boolean().default(true),
1202
+ }).default({}),
1203
+ stealth: z.object({
1204
+ enabled: z.boolean().default(true),
1205
+ randomizeViewport: z.boolean().default(true),
1206
+ randomizeTiming: z.boolean().default(true),
1207
+ minDelay: z.number().int().min(0).default(500),
1208
+ maxDelay: z.number().int().min(0).default(2000),
1209
+ }).default({}),
1210
+ errorHandling: z.object({
1211
+ maxConsecutiveErrors: z.number().int().min(1).default(5),
1212
+ circuitBreakerThreshold: z.number().min(0).max(1).default(0.5),
1213
+ circuitBreakerResetTimeout: z.number().int().min(1000).default(60_000),
1214
+ }).default({}),
1215
+ }).strict();
1216
+
1217
+ // ============================================
1218
+ // Validation Utility
1219
+ // ============================================
1220
+
1221
+ class ValidationError extends Error {
1222
+ public readonly issues: z.ZodIssue[];
1223
+
1224
+ constructor(error: z.ZodError) {
1225
+ const message = error.issues
1226
+ .map(i => `${i.path.join('.')}: ${i.message}`)
1227
+ .join('\n');
1228
+ super(`Validation failed:\n${message}`);
1229
+ this.name = 'ValidationError';
1230
+ this.issues = error.issues;
1231
+ }
1232
+
1233
+ toJSON() {
1234
+ return {
1235
+ name: this.name,
1236
+ message: this.message,
1237
+ issues: this.issues.map(i => ({
1238
+ path: i.path.join('.'),
1239
+ message: i.message,
1240
+ code: i.code,
1241
+ })),
1242
+ };
1243
+ }
1244
+ }
1245
+
1246
+ function validate<T extends z.ZodSchema>(schema: T, data: unknown): z.infer<T> {
1247
+ const result = schema.safeParse(data);
1248
+ if (!result.success) {
1249
+ throw new ValidationError(result.error);
1250
+ }
1251
+ return result.data;
1252
+ }
1253
+
1254
+ // ============================================
1255
+ // Usage
1256
+ // ============================================
1257
+
1258
+ // Single URL scrape
1259
+ const options = validate(ScrapeOptionsSchema, {
1260
+ url: 'https://example.com',
1261
+ concurrency: 3,
1262
+ proxy: {
1263
+ url: 'http://proxy.example.com:8080',
1264
+ type: 'http',
1265
+ },
1266
+ });
1267
+
1268
+ // Batch scrape
1269
+ const batch = validate(BatchConfigSchema, {
1270
+ urls: [
1271
+ 'https://example.com/page1',
1272
+ 'https://example.com/page2',
1273
+ 'https://example.com/page3',
1274
+ ],
1275
+ concurrency: 5,
1276
+ defaultOptions: {
1277
+ headless: true,
1278
+ maxRetries: 3,
1279
+ },
1280
+ });
1281
+
1282
+ // Browser agent config
1283
+ const agentConfig = validate(BrowserAgentConfigSchema, {
1284
+ browserPool: {
1285
+ maxBrowsers: 3,
1286
+ maxPagesPerBrowser: 15,
1287
+ },
1288
+ stealth: {
1289
+ enabled: true,
1290
+ randomizeTiming: true,
1291
+ },
1292
+ });
1293
+
1294
+ // CLI args validation
1295
+ const cliOptions = validate(
1296
+ z.object({
1297
+ url: z.string().url(),
1298
+ concurrency: z.coerce.number().int().min(1).max(100).default(5),
1299
+ headless: z.coerce.boolean().default(true),
1300
+ verbose: z.coerce.boolean().default(false),
1301
+ }),
1302
+ {
1303
+ url: process.argv[2],
1304
+ concurrency: process.argv[3],
1305
+ headless: process.argv[4],
1306
+ verbose: process.argv[5],
1307
+ }
1308
+ );
1309
+
1310
+ // API request validation (Express middleware)
1311
+ function validateBody<T extends z.ZodSchema>(schema: T) {
1312
+ return (req: any, res: any, next: any) => {
1313
+ const result = schema.safeParse(req.body);
1314
+ if (!result.success) {
1315
+ return res.status(400).json({
1316
+ error: 'Validation failed',
1317
+ details: result.error.issues.map(i => ({
1318
+ path: i.path.join('.'),
1319
+ message: i.message,
1320
+ })),
1321
+ });
1322
+ }
1323
+ req.body = result.data;
1324
+ next();
1325
+ };
1326
+ }
1327
+
1328
+ // app.post('/api/scrape', validateBody(ScrapeOptionsSchema), handler);
1329
+ ```
1330
+
1331
+ ---
1332
+
1333
+ ## Summary: Quick Reference
1334
+
1335
+ | Task | Zod API | Example |
1336
+ |------|---------|---------|
1337
+ | Basic object | `z.object({...})` | `z.object({ url: z.string() })` |
1338
+ | String URL | `z.string().url()` | Validates URL format |
1339
+ | Number with bounds | `z.number().min().max()` | `z.number().int().min(1).max(100)` |
1340
+ | Enum | `z.enum([...])` | `z.enum(['a', 'b', 'c'])` |
1341
+ | Default values | `.default(value)` | `z.number().default(5)` |
1342
+ | Optional | `.optional()` | `z.string().optional()` |
1343
+ | Coerce from string | `z.coerce.number()` | CLI arguments |
1344
+ | Transform | `.transform(fn)` | Parse string to Date |
1345
+ | Refine | `.refine(fn, msg)` | Cross-field validation |
1346
+ | Discriminated union | `z.discriminatedUnion()` | Different response types |
1347
+ | Strict object | `z.strictObject()` | Reject unknown keys |
1348
+ | Derive TS type | `z.infer<typeof schema>` | Type from schema |
1349
+ | Safe parse | `.safeParse(data)` | Returns result, no throw |
1350
+ | Generate JSON Schema | `z.toJSONSchema()` | v4 only |