@strayl/agent 0.1.3 → 0.1.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 (57) hide show
  1. package/package.json +5 -1
  2. package/skills/api-creation/SKILL.md +631 -0
  3. package/skills/authentication/SKILL.md +294 -0
  4. package/skills/frontend-design/SKILL.md +108 -0
  5. package/skills/landing-creation/SKILL.md +125 -0
  6. package/skills/reference/SKILL.md +149 -0
  7. package/skills/web-application-creation/SKILL.md +231 -0
  8. package/src/agent.ts +0 -465
  9. package/src/checkpoints/manager.ts +0 -112
  10. package/src/context/manager.ts +0 -185
  11. package/src/context/summarizer.ts +0 -104
  12. package/src/context/trim.ts +0 -55
  13. package/src/emitter.ts +0 -14
  14. package/src/hitl/manager.ts +0 -77
  15. package/src/hitl/transport.ts +0 -13
  16. package/src/index.ts +0 -116
  17. package/src/llm/client.ts +0 -276
  18. package/src/llm/gemini-native.ts +0 -307
  19. package/src/llm/models.ts +0 -64
  20. package/src/middleware/compose.ts +0 -24
  21. package/src/middleware/credential-scrubbing.ts +0 -31
  22. package/src/middleware/forbidden-packages.ts +0 -107
  23. package/src/middleware/plan-mode.ts +0 -143
  24. package/src/middleware/prompt-caching.ts +0 -21
  25. package/src/middleware/tool-compression.ts +0 -25
  26. package/src/middleware/tool-filter.ts +0 -13
  27. package/src/prompts/implementation-mode.md +0 -16
  28. package/src/prompts/plan-mode.md +0 -51
  29. package/src/prompts/system.ts +0 -173
  30. package/src/skills/loader.ts +0 -53
  31. package/src/stdin-listener.ts +0 -61
  32. package/src/subagents/definitions.ts +0 -72
  33. package/src/subagents/manager.ts +0 -161
  34. package/src/todos/manager.ts +0 -61
  35. package/src/tools/builtin/delete.ts +0 -29
  36. package/src/tools/builtin/edit.ts +0 -74
  37. package/src/tools/builtin/exec.ts +0 -216
  38. package/src/tools/builtin/glob.ts +0 -104
  39. package/src/tools/builtin/grep.ts +0 -115
  40. package/src/tools/builtin/ls.ts +0 -54
  41. package/src/tools/builtin/move.ts +0 -31
  42. package/src/tools/builtin/read.ts +0 -69
  43. package/src/tools/builtin/write.ts +0 -42
  44. package/src/tools/executor.ts +0 -51
  45. package/src/tools/external/database.ts +0 -285
  46. package/src/tools/external/enter-plan-mode.ts +0 -34
  47. package/src/tools/external/generate-image.ts +0 -110
  48. package/src/tools/external/hitl-tools.ts +0 -118
  49. package/src/tools/external/preview.ts +0 -28
  50. package/src/tools/external/proxy-fetch.ts +0 -51
  51. package/src/tools/external/task.ts +0 -38
  52. package/src/tools/external/wait.ts +0 -20
  53. package/src/tools/external/web-fetch.ts +0 -57
  54. package/src/tools/external/web-search.ts +0 -61
  55. package/src/tools/registry.ts +0 -36
  56. package/src/tools/zod-to-json-schema.ts +0 -86
  57. package/src/types.ts +0 -151
package/package.json CHANGED
@@ -1,11 +1,15 @@
1
1
  {
2
2
  "name": "@strayl/agent",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
8
8
  "main": "dist/index.js",
9
+ "files": [
10
+ "dist",
11
+ "skills"
12
+ ],
9
13
  "scripts": {
10
14
  "build": "esbuild src/index.ts --bundle --platform=node --target=node20 --format=esm --outfile=dist/agent.js --external:fsevents",
11
15
  "dev": "tsx watch src/index.ts"
@@ -0,0 +1,631 @@
1
+ ---
2
+ name: api-creation
3
+ description: Create server-side API logic in a TanStack Start app using createServerFn, Drizzle ORM, and Zod validation. Use when the user needs backend endpoints, CRUD operations, data fetching, or server-side business logic.
4
+ ---
5
+
6
+ # API Creation
7
+
8
+ ## When to Use
9
+
10
+ - User asks to create an API, endpoint, server function, or backend logic
11
+ - User needs CRUD operations (create, read, update, delete) for any entity
12
+ - User wants to fetch data from the database and display it
13
+ - User mentions "server function", "API", "backend", "endpoint", "CRUD"
14
+ - User asks to "save data", "fetch data", "submit a form", "load from database"
15
+
16
+ ## When NOT to Use
17
+
18
+ - User only needs client-side state (React state, localStorage, URL search params)
19
+ - User is asking about authentication specifically — use the `authentication` skill
20
+ - User wants to scaffold a new project — use `web-application-creation` skill
21
+ - User only needs to create/modify database schema — use `database-management` skill
22
+
23
+ ## Prerequisites
24
+
25
+ - A TanStack Start project (scaffolded with `web-application-creation` or existing)
26
+ - A database with tables (via `database-management` skill)
27
+ - If endpoints need auth protection, set up auth first with `authentication` skill
28
+
29
+ ## ⛔ RULE #1 — NEVER Use Dynamic Imports for Server Functions
30
+
31
+ This is the most common mistake. It **breaks the build** every time.
32
+
33
+ ```typescript
34
+ // ❌ BROKEN — dynamic import inside useEffect, onClick, or any async callback
35
+ useEffect(() => {
36
+ const load = async () => {
37
+ const { getTasks } = await import('../server/tasks') // BUILD ERROR
38
+ setTasks(await getTasks({ userId }))
39
+ }
40
+ load()
41
+ }, [userId])
42
+
43
+ // ❌ BROKEN — same problem inside an event handler
44
+ const handleCreate = async () => {
45
+ const { createTask } = await import('../server/tasks') // BUILD ERROR
46
+ await createTask({ ... })
47
+ }
48
+ ```
49
+
50
+ ```typescript
51
+ // ✅ CORRECT — static import at top of file, always
52
+ import { getTasks } from '@/server/tasks'
53
+ import { createTask } from '@/server/tasks'
54
+ import { useServerFn } from '@tanstack/react-start'
55
+
56
+ // GET data → call in route loader
57
+ export const Route = createFileRoute('/tasks')({
58
+ loader: ({ context }) => getTasks({ userId: context.userId }),
59
+ component: TasksPage,
60
+ })
61
+
62
+ // POST mutation → wrap with useServerFn() in component
63
+ function TasksPage() {
64
+ const create = useServerFn(createTask)
65
+ const handleCreate = async () => {
66
+ await create({ data: { title: 'New task', userId } })
67
+ }
68
+ }
69
+ ```
70
+
71
+ **Why it breaks:** TanStack Start analyzes static imports at build time to split client/server bundles. Dynamic `await import()` bypasses this — the bundler fails or leaks server-only code (DB, env vars) to the client.
72
+
73
+ ## ⛔ RULE #2 — Do NOT Use useEffect for Data Loading
74
+
75
+ Never load server data inside `useEffect`. Always use route loaders:
76
+
77
+ ```typescript
78
+ // ❌ BROKEN pattern — data fetching in useEffect
79
+ function TasksPage() {
80
+ const [tasks, setTasks] = useState([])
81
+ useEffect(() => {
82
+ // DON'T DO THIS — use loader instead
83
+ getTasks({ userId }).then(setTasks)
84
+ }, [userId])
85
+ }
86
+
87
+ // ✅ CORRECT — data in loader, consumed with useLoaderData()
88
+ export const Route = createFileRoute('/tasks')({
89
+ loader: ({ context }) => getTasks({ userId: context.userId }),
90
+ component: TasksPage,
91
+ })
92
+ function TasksPage() {
93
+ const tasks = Route.useLoaderData() // no useEffect needed
94
+ }
95
+ ```
96
+
97
+ ## How to Pass Auth Context to Loaders
98
+
99
+ When you need `userId` (or any auth data) in a route loader, use the router context — set it once in `__root.tsx` and it flows to all routes automatically:
100
+
101
+ ```typescript
102
+ // src/routes/__root.tsx — use createRootRouteWithContext for TypeScript safety
103
+ import { createRootRouteWithContext } from '@tanstack/react-router'
104
+ import { getUser } from '@/server/auth'
105
+
106
+ interface RouterContext {
107
+ user: Awaited<ReturnType<typeof getUser>> | null
108
+ }
109
+
110
+ export const Route = createRootRouteWithContext<RouterContext>()({
111
+ beforeLoad: async () => {
112
+ const user = await getUser()
113
+ return { user } // available as context.user in ALL route loaders
114
+ },
115
+ // ...
116
+ })
117
+
118
+ // src/routes/tasks.tsx — access user in loader (no useEffect needed)
119
+ import { getTasks } from '@/server/tasks'
120
+ export const Route = createFileRoute('/tasks')({
121
+ loader: ({ context }) => {
122
+ if (!context.user) return []
123
+ return getTasks({ userId: context.user.id })
124
+ },
125
+ component: TasksPage,
126
+ })
127
+ function TasksPage() {
128
+ const tasks = Route.useLoaderData() // data from loader, fully typed
129
+ const { user } = Route.useRouteContext() // auth context in component
130
+ }
131
+ ```
132
+
133
+ This eliminates the need for useEffect-based data loading entirely.
134
+
135
+ ## Core API: createServerFn
136
+
137
+ All backend logic goes through `createServerFn` from `@tanstack/react-start`. Server functions run on the server (Nitro) and are callable from loaders and client components.
138
+
139
+ ```typescript
140
+ // ✅ CORRECT import path
141
+ import { createServerFn } from "@tanstack/react-start";
142
+ import { useServerFn } from "@tanstack/react-start";
143
+
144
+ // ❌ WRONG — /server subpath is for entry-point utilities only (createStartHandler, setResponseStatus)
145
+ import { createServerFn } from "@tanstack/react-start/server"; // BUILD ERROR
146
+ ```
147
+
148
+ **Fluent API chain:** `createServerFn({ method }) .inputValidator(fn) .handler(fn)`
149
+
150
+ - `method: "GET"` — for reads (loaders, data fetching)
151
+ - `method: "POST"` — for mutations (create, update, delete)
152
+ - `.inputValidator()` — validates and types input (use zod)
153
+ - `.handler()` — receives `{ data }` (validated input), returns response
154
+
155
+ ## File Organization
156
+
157
+ ```
158
+ src/
159
+ ├── lib/
160
+ │ ├── db.ts # Drizzle client (single instance)
161
+ │ └── schema.ts # All table definitions
162
+ ├── server/
163
+ │ ├── auth.ts # Auth functions (if authentication skill used)
164
+ │ ├── posts.ts # Post server functions
165
+ │ ├── comments.ts # Comment server functions
166
+ │ └── {entity}.ts # One file per entity/domain
167
+ └── routes/
168
+ ├── posts.tsx # List page (loader calls GET server fn)
169
+ ├── posts.$postId.tsx # Detail page (loader with params)
170
+ └── _authed/
171
+ └── posts.new.tsx # Create form (calls POST server fn)
172
+ ```
173
+
174
+ **Convention:** Server functions in `src/server/{entity}.ts`. Routes import and call them. Keeps server logic separate from UI.
175
+
176
+ ## Setup (if not done)
177
+
178
+ ### Database Client
179
+
180
+ **Check first** — `src/lib/db.ts` may already exist from `authentication` or `database-management`. If not:
181
+
182
+ ```typescript
183
+ // src/lib/db.ts
184
+ import { neon } from "@neondatabase/serverless";
185
+ import { drizzle } from "drizzle-orm/neon-http";
186
+
187
+ let _db: ReturnType<typeof drizzle>;
188
+ export function db() {
189
+ if (!_db) {
190
+ _db = drizzle(neon(process.env.DATABASE_URL!));
191
+ }
192
+ return _db;
193
+ }
194
+ ```
195
+
196
+ ### Schema
197
+
198
+ Add table definitions to `src/lib/schema.ts` (edit if exists, create if not):
199
+
200
+ ```typescript
201
+ import { pgTable, serial, text, timestamp, integer, boolean } from "drizzle-orm/pg-core";
202
+
203
+ export const posts = pgTable("posts", {
204
+ id: serial("id").primaryKey(),
205
+ title: text("title").notNull(),
206
+ content: text("content").notNull().default(""),
207
+ authorId: integer("author_id").notNull(),
208
+ published: boolean("published").notNull().default(false),
209
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
210
+ updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
211
+ });
212
+ ```
213
+
214
+ After defining schema in code, **create the actual table** via migration:
215
+ ```
216
+ universal({ tool: "prepare_database_migration", params: { migrationSql: "CREATE TABLE IF NOT EXISTS posts (...)" } })
217
+ ```
218
+
219
+ ## Patterns
220
+
221
+ ### GET — List All
222
+
223
+ ```typescript
224
+ // src/server/posts.ts
225
+ import { createServerFn } from "@tanstack/react-start";
226
+ import { db } from "@/lib/db";
227
+ import { posts } from "@/lib/schema";
228
+ import { desc, eq } from "drizzle-orm";
229
+
230
+ export const getPosts = createServerFn({ method: "GET" }).handler(async () => {
231
+ return db().select().from(posts).orderBy(desc(posts.createdAt));
232
+ });
233
+ ```
234
+
235
+ **In a route loader:**
236
+ ```typescript
237
+ // src/routes/posts.tsx
238
+ import { createFileRoute } from "@tanstack/react-router";
239
+ import { getPosts } from "@/server/posts";
240
+
241
+ export const Route = createFileRoute("/posts")({
242
+ loader: () => getPosts(),
243
+ component: PostsPage,
244
+ });
245
+
246
+ function PostsPage() {
247
+ const posts = Route.useLoaderData();
248
+ return (
249
+ <div className="container mx-auto p-6">
250
+ {posts.map((post) => (
251
+ <div key={post.id}>{post.title}</div>
252
+ ))}
253
+ </div>
254
+ );
255
+ }
256
+ ```
257
+
258
+ ### GET — Single by ID
259
+
260
+ ```typescript
261
+ export const getPost = createServerFn({ method: "GET" })
262
+ .inputValidator((id: number) => id)
263
+ .handler(async ({ data: id }) => {
264
+ const [post] = await db().select().from(posts).where(eq(posts.id, id)).limit(1);
265
+ if (!post) throw new Error("Post not found");
266
+ return post;
267
+ });
268
+ ```
269
+
270
+ **In a route with params:**
271
+ ```typescript
272
+ // src/routes/posts.$postId.tsx
273
+ export const Route = createFileRoute("/posts/$postId")({
274
+ loader: ({ params }) => getPost({ data: Number(params.postId) }),
275
+ component: PostPage,
276
+ });
277
+ ```
278
+
279
+ ### POST — Create with Validation
280
+
281
+ ```typescript
282
+ import { z } from "zod";
283
+
284
+ const createPostSchema = z.object({
285
+ title: z.string().min(1).max(200),
286
+ content: z.string().max(50000).default(""),
287
+ });
288
+
289
+ export const createPost = createServerFn({ method: "POST" })
290
+ .inputValidator((d: unknown) => createPostSchema.parse(d))
291
+ .handler(async ({ data }) => {
292
+ const [post] = await db()
293
+ .insert(posts)
294
+ .values({ title: data.title, content: data.content, authorId: 1 })
295
+ .returning();
296
+ return post;
297
+ });
298
+ ```
299
+
300
+ **Calling from a component:**
301
+ ```typescript
302
+ import { useServerFn } from "@tanstack/react-start";
303
+ import { useNavigate } from "@tanstack/react-router";
304
+ import { createPost } from "@/server/posts";
305
+ import { Button } from "@/components/ui/button";
306
+ import { Input } from "@/components/ui/input";
307
+ import { Label } from "@/components/ui/label";
308
+
309
+ function CreatePostForm() {
310
+ const navigate = useNavigate();
311
+ const create = useServerFn(createPost);
312
+ const [error, setError] = useState("");
313
+
314
+ async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
315
+ e.preventDefault();
316
+ setError("");
317
+ const formData = new FormData(e.currentTarget);
318
+ try {
319
+ const post = await create({
320
+ data: {
321
+ title: formData.get("title") as string,
322
+ content: formData.get("content") as string,
323
+ },
324
+ });
325
+ navigate({ to: "/posts/$postId", params: { postId: String(post.id) } });
326
+ } catch (err) {
327
+ setError(err instanceof Error ? err.message : "Something went wrong");
328
+ }
329
+ }
330
+
331
+ return (
332
+ <form onSubmit={handleSubmit} className="space-y-4">
333
+ {error && <p className="text-sm text-destructive">{error}</p>}
334
+ <div className="space-y-2">
335
+ <Label htmlFor="title">Title</Label>
336
+ <Input id="title" name="title" required />
337
+ </div>
338
+ <div className="space-y-2">
339
+ <Label htmlFor="content">Content</Label>
340
+ <textarea id="content" name="content" className="w-full rounded-md border bg-background p-3" />
341
+ </div>
342
+ <Button type="submit">Create Post</Button>
343
+ </form>
344
+ );
345
+ }
346
+ ```
347
+
348
+ ### POST — Update
349
+
350
+ ```typescript
351
+ const updatePostSchema = z.object({
352
+ id: z.number(),
353
+ title: z.string().min(1).max(200).optional(),
354
+ content: z.string().max(50000).optional(),
355
+ published: z.boolean().optional(),
356
+ });
357
+
358
+ export const updatePost = createServerFn({ method: "POST" })
359
+ .inputValidator((d: unknown) => updatePostSchema.parse(d))
360
+ .handler(async ({ data }) => {
361
+ const { id, ...updates } = data;
362
+ const setValues: Record<string, unknown> = { updatedAt: new Date() };
363
+ if (updates.title !== undefined) setValues.title = updates.title;
364
+ if (updates.content !== undefined) setValues.content = updates.content;
365
+ if (updates.published !== undefined) setValues.published = updates.published;
366
+
367
+ const [updated] = await db().update(posts).set(setValues).where(eq(posts.id, id)).returning();
368
+ if (!updated) throw new Error("Post not found");
369
+ return updated;
370
+ });
371
+ ```
372
+
373
+ ### POST — Delete
374
+
375
+ ```typescript
376
+ export const deletePost = createServerFn({ method: "POST" })
377
+ .inputValidator((d: unknown) => z.object({ id: z.number() }).parse(d))
378
+ .handler(async ({ data }) => {
379
+ const [deleted] = await db().delete(posts).where(eq(posts.id, data.id)).returning({ id: posts.id });
380
+ if (!deleted) throw new Error("Post not found");
381
+ return { success: true };
382
+ });
383
+ ```
384
+
385
+ ### Auth-Protected Server Functions
386
+
387
+ When `authentication` skill is set up, check session:
388
+
389
+ ```typescript
390
+ import { useAppSession } from "@/lib/session";
391
+
392
+ export const createPost = createServerFn({ method: "POST" })
393
+ .inputValidator((d: unknown) => createPostSchema.parse(d))
394
+ .handler(async ({ data }) => {
395
+ const session = await useAppSession();
396
+ if (!session.data.userId) {
397
+ return { error: "Not authenticated" };
398
+ }
399
+
400
+ const [post] = await db()
401
+ .insert(posts)
402
+ .values({ title: data.title, content: data.content, authorId: session.data.userId })
403
+ .returning();
404
+ return post;
405
+ });
406
+ ```
407
+
408
+ For GET functions in loaders, return empty instead of throwing:
409
+ ```typescript
410
+ export const getMyPosts = createServerFn({ method: "GET" }).handler(async () => {
411
+ const session = await useAppSession();
412
+ if (!session.data.userId) return [];
413
+ return db().select().from(posts).where(eq(posts.authorId, session.data.userId)).orderBy(desc(posts.createdAt));
414
+ });
415
+ ```
416
+
417
+ ### Pagination
418
+
419
+ ```typescript
420
+ import { sql } from "drizzle-orm";
421
+
422
+ const listSchema = z.object({
423
+ page: z.number().int().min(1).default(1),
424
+ limit: z.number().int().min(1).max(100).default(20),
425
+ });
426
+
427
+ export const listPosts = createServerFn({ method: "GET" })
428
+ .inputValidator((d: unknown) => listSchema.parse(d))
429
+ .handler(async ({ data }) => {
430
+ const offset = (data.page - 1) * data.limit;
431
+ const [rows, countResult] = await Promise.all([
432
+ db().select().from(posts).orderBy(desc(posts.createdAt)).limit(data.limit).offset(offset),
433
+ db().select({ count: sql<number>`count(*)::int` }).from(posts),
434
+ ]);
435
+ return {
436
+ items: rows,
437
+ total: countResult[0].count,
438
+ page: data.page,
439
+ totalPages: Math.ceil(countResult[0].count / data.limit),
440
+ };
441
+ });
442
+ ```
443
+
444
+ ### Search / Filter
445
+
446
+ ```typescript
447
+ import { ilike, and } from "drizzle-orm";
448
+
449
+ export const searchPosts = createServerFn({ method: "GET" })
450
+ .inputValidator((d: unknown) => z.object({ query: z.string().default("") }).parse(d))
451
+ .handler(async ({ data }) => {
452
+ const conditions = [];
453
+ if (data.query) conditions.push(ilike(posts.title, `%${data.query}%`));
454
+ return db()
455
+ .select().from(posts)
456
+ .where(conditions.length > 0 ? and(...conditions) : undefined)
457
+ .orderBy(desc(posts.createdAt))
458
+ .limit(50);
459
+ });
460
+ ```
461
+
462
+ ### JOIN (Relations)
463
+
464
+ ```typescript
465
+ export const getPostWithAuthor = createServerFn({ method: "GET" })
466
+ .inputValidator((id: number) => id)
467
+ .handler(async ({ data: id }) => {
468
+ const [result] = await db()
469
+ .select({
470
+ id: posts.id, title: posts.title, content: posts.content, createdAt: posts.createdAt,
471
+ author: { id: users.id, name: users.name, email: users.email },
472
+ })
473
+ .from(posts)
474
+ .innerJoin(users, eq(posts.authorId, users.id))
475
+ .where(eq(posts.id, id))
476
+ .limit(1);
477
+ if (!result) throw new Error("Post not found");
478
+ return result;
479
+ });
480
+ ```
481
+
482
+ ## Workflow
483
+
484
+ When the user asks to create backend logic for an entity:
485
+
486
+ 1. **Understand the entity** — fields, access rules, operations needed
487
+ 2. **Check existing setup:**
488
+ ```
489
+ read_file("src/lib/db.ts") # DB client exists?
490
+ read_file("src/lib/schema.ts") # Existing tables?
491
+ ls("src/server") # Existing server functions?
492
+ ```
493
+ 3. **Create/migrate table** — `database-management` tools
494
+ 4. **Install deps if needed** — `@neondatabase/serverless drizzle-orm zod`
495
+ 5. **Add schema definition** — edit `src/lib/schema.ts`
496
+ 6. **Create server functions** — `src/server/{entity}.ts`
497
+ 7. **Create route files** — loaders + components using the server functions
498
+ 8. **Run build to catch import errors:**
499
+ ```
500
+ npm run build
501
+ ```
502
+ Fix any errors before continuing. Build errors here almost always mean a dynamic import or a server-only import leaking into client code.
503
+ 9. **Show preview**
504
+
505
+ ## Error Handling
506
+
507
+ ### In server functions — return errors for expected cases, throw for unexpected:
508
+
509
+ ```typescript
510
+ export const createPost = createServerFn({ method: "POST" })
511
+ .inputValidator((d: unknown) => createPostSchema.parse(d))
512
+ .handler(async ({ data }) => {
513
+ try {
514
+ const [post] = await db().insert(posts).values(data).returning();
515
+ return post;
516
+ } catch (err: any) {
517
+ // Handle unique constraint violations gracefully
518
+ if (err.message?.includes("unique constraint")) {
519
+ return { error: "A post with this title already exists" };
520
+ }
521
+ throw err; // Re-throw unexpected errors
522
+ }
523
+ });
524
+ ```
525
+
526
+ ### In components — check for error in result:
527
+
528
+ ```typescript
529
+ const result = await create({ data: formValues });
530
+ if (result && "error" in result) {
531
+ setError(result.error);
532
+ return;
533
+ }
534
+ // Success — navigate or update UI
535
+ ```
536
+
537
+ ### Zod validation errors — catch in form handler:
538
+
539
+ ```typescript
540
+ try {
541
+ await create({ data: formValues });
542
+ } catch (err) {
543
+ if (err instanceof z.ZodError) {
544
+ setError(err.errors.map(e => e.message).join(", "));
545
+ } else {
546
+ setError(err instanceof Error ? err.message : "Something went wrong");
547
+ }
548
+ }
549
+ ```
550
+
551
+ ## Drizzle Quick Reference
552
+
553
+ ### SQL-to-Drizzle Type Mapping
554
+
555
+ | SQL Type | Drizzle | Notes |
556
+ |----------|---------|-------|
557
+ | `SERIAL` | `serial("col")` | Auto-increment integer PK |
558
+ | `TEXT` | `text("col")` | Unbounded string |
559
+ | `VARCHAR(N)` | `varchar("col", { length: N })` | Bounded string |
560
+ | `INTEGER` | `integer("col")` | 32-bit integer |
561
+ | `BOOLEAN` | `boolean("col")` | true/false |
562
+ | `TIMESTAMPTZ` | `timestamp("col", { withTimezone: true })` | Always use `withTimezone: true` |
563
+ | `JSONB` | `jsonb("col")` | JSON column |
564
+ | `TEXT[]` | `text("col").array()` | Array column |
565
+ | `REAL` | `real("col")` | Floating point |
566
+
567
+ ### Query Operations
568
+
569
+ ```typescript
570
+ import { eq, ne, gt, gte, lt, lte, and, or, ilike, desc, asc, sql } from "drizzle-orm";
571
+
572
+ // Select
573
+ await db().select().from(posts);
574
+ await db().select().from(posts).where(eq(posts.id, 1));
575
+ await db().select({ id: posts.id, title: posts.title }).from(posts);
576
+
577
+ // Insert
578
+ const [post] = await db().insert(posts).values({ title: "Hi", authorId: 1 }).returning();
579
+
580
+ // Batch insert
581
+ await db().insert(posts).values([{ title: "A", authorId: 1 }, { title: "B", authorId: 1 }]).returning();
582
+
583
+ // Upsert (create or update)
584
+ await db().insert(posts).values({ id: 1, title: "Updated" })
585
+ .onConflictDoUpdate({ target: posts.id, set: { title: "Updated", updatedAt: new Date() } })
586
+ .returning();
587
+
588
+ // Update
589
+ const [updated] = await db().update(posts).set({ title: "New" }).where(eq(posts.id, 1)).returning();
590
+
591
+ // Delete
592
+ const [deleted] = await db().delete(posts).where(eq(posts.id, 1)).returning({ id: posts.id });
593
+
594
+ // Join
595
+ await db().select().from(posts).innerJoin(users, eq(posts.authorId, users.id));
596
+
597
+ // Count
598
+ await db().select({ count: sql<number>`count(*)::int` }).from(posts);
599
+ ```
600
+
601
+ ## Zod Quick Reference
602
+
603
+ ```typescript
604
+ z.string().min(1).max(200) // required bounded string
605
+ z.string().email() // email validation
606
+ z.string().url() // URL validation
607
+ z.string().uuid() // UUID validation
608
+ z.string().regex(/^[a-z]+$/) // regex pattern
609
+ z.number().int().min(1) // positive integer
610
+ z.boolean().default(false) // boolean with default
611
+ z.string().optional() // optional
612
+ z.enum(["draft", "published"]) // enum
613
+ z.array(z.string()) // array
614
+ z.object({ key: z.string() }) // nested object
615
+ z.union([z.string(), z.number()]) // union type
616
+ z.string().transform(s => s.trim()) // transform value
617
+ ```
618
+
619
+ **Always use `.inputValidator((d: unknown) => schema.parse(d))`** for proper type inference + runtime validation.
620
+
621
+ ## Important Notes
622
+
623
+ - **Server functions run on the server only.** Database imports and `process.env` are safe inside `.handler()`.
624
+ - **Validators run on both client and server.** Keep them pure — no DB calls, no `process.env`.
625
+ - **GET** functions are for loaders. **POST** functions are for mutations via `useServerFn()`.
626
+ - **Do NOT create Express-style API routes.** `createServerFn` replaces the need for a separate API server.
627
+ - **Do NOT use `fetch("/api/...")`** patterns. Call server functions directly.
628
+ - **NEVER use `await import()` to load server functions** — always static imports at file top. Dynamic imports break TanStack Start's build-time bundle analysis and cause hard-to-debug build failures.
629
+ - **Schema in code must match the database.** After adding to `src/lib/schema.ts`, create the table via migration tools.
630
+ - **Run `npm run build` after writing first server function + route.** Catches import errors early before you write more code on top of a broken foundation.
631
+ - Use existing template UI components (Button, Input, Label, Card, etc.) for forms and data display. Add more via `npx shadcn@latest add <component>` — see `web-application-creation` skill for the full list.