Data mutations
Forms, validation, and round-tripping to Python.
Forms
BionicJS recommends using standard React Server Functions (or equivalent mutation patterns like React Query) to send data from the client to the server.
1const result = await api.users.updateProfile({
2 id: "user_123",
3 name: form.name,
4});
Mutations are typed RPC calls, the same as any other fetch. The form submits to server/api/ endpoints, where input is validated using Zod before being processed by the h3 handler and passed down to the database or Python layer.
Validation
Validation happens at the server boundary, not in the component. The schema that validates the input is the same schema that types the call - so what the client sends is what the server checked:
1const UpdateProfileSchema = z.object({
2 id: z.string(),
3 name: z.string().min(1).max(80),
4});
5
6export default defineEventHandler(async (event) => {
7 const body = await readValidatedBody(event, UpdateProfileSchema.safeParse);
8 const row = await db.update(users)
9 .set({ name: body.name })
10 .where(eq(users.id, body.id))
11 .returning();
12 return row;
13});
The flow is predictable: the client sends a typed payload, the server validates it against the schema, and an invalid request fails fast - before any database write or Python roundtrip.
To Python
When the mutation needs intelligence - a generated embedding, a content rewrite, a classification - it crosses to Python through the typed RPC boundary:
1export default defineEventHandler(async (event) => {
2 const body = await readValidatedBody(event, UpdateProfileSchema.safeParse);
3 const summary = await generateSummary(body.bio); // -> Python RPC
4 const row = await db.update(profiles)
5 .set({ summary })
6 .where(eq(profiles.id, body.id));
7 return row;
8});
The mutation is a single handler. The database write and the Python call are both typed, both contained in one file, and both executed in one request - no second service, no untyped JSON handshake.
23 / 25