next-action-forge 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,434 @@
1
+ # Next Action Forge
2
+
3
+ A powerful, type-safe toolkit for Next.js server actions with Zod validation and class-based API design.
4
+
5
+ ## ✨ Features
6
+
7
+ - 🚀 **Type-safe server actions** with full TypeScript support
8
+ - 🎯 **Zod validation** built-in with perfect type inference
9
+ - 🏗️ **Class-based API** with intuitive method chaining
10
+ - 🪝 **React hooks** for seamless client-side integration
11
+ - 🔄 **Optimistic updates** support
12
+ - 🔐 **Middleware system** with context propagation
13
+ - ⚡ **Zero config** - works out of the box
14
+ - 🎨 **Custom error handling** with flexible error transformation
15
+ - 🦆 **Duck-typed errors** - Any error with `toServerActionError()` method is automatically handled
16
+ - 📋 **Smart FormData parsing** - Handles arrays, checkboxes, and files correctly
17
+ - ✅ **React 19 & Next.js 15** compatible
18
+
19
+ ## 📦 Installation
20
+
21
+ ```bash
22
+ npm install next-action-forge
23
+ # or
24
+ yarn add next-action-forge
25
+ # or
26
+ pnpm add next-action-forge
27
+ ```
28
+
29
+ ### Requirements
30
+ - Next.js 14.0.0 or higher
31
+ - React 18.0.0 or higher
32
+ - Zod 4.0.0 or higher
33
+
34
+ ## 🚀 Quick Start
35
+
36
+ ### 1. Create Server Actions
37
+
38
+ ```typescript
39
+ // app/actions/user.ts
40
+ "use server";
41
+
42
+ import { createActionClient } from "next-action-forge";
43
+ import { z } from "zod";
44
+
45
+ // Create a reusable client
46
+ const actionClient = createActionClient();
47
+
48
+ // Define input schema
49
+ const userSchema = z.object({
50
+ name: z.string().min(2),
51
+ email: z.string().email(),
52
+ });
53
+
54
+ // Create an action with method chaining
55
+ export const createUser = actionClient
56
+ .inputSchema(userSchema)
57
+ .onError((error) => {
58
+ console.error("Failed to create user:", error);
59
+ return {
60
+ code: "USER_CREATE_ERROR",
61
+ message: "Failed to create user. Please try again.",
62
+ };
63
+ })
64
+ .action(async ({ name, email }) => {
65
+ // Your server logic here
66
+ const user = await db.user.create({
67
+ data: { name, email },
68
+ });
69
+
70
+ return user;
71
+ });
72
+
73
+ // Action without input
74
+ export const getServerTime = actionClient
75
+ .action(async () => {
76
+ return { time: new Date().toISOString() };
77
+ });
78
+ ```
79
+
80
+ ### 2. Use in Client Components
81
+
82
+ ```tsx
83
+ // app/users/create-user-form.tsx
84
+ "use client";
85
+
86
+ import { useServerAction } from "next-action-forge/hooks";
87
+ import { createUser } from "@/app/actions/user";
88
+ import { toast } from "sonner";
89
+
90
+ export function CreateUserForm() {
91
+ const { execute, isLoading } = useServerAction(createUser, {
92
+ onSuccess: (data) => {
93
+ toast.success(`User ${data.name} created!`);
94
+ },
95
+ onError: (error) => {
96
+ toast.error(error.message);
97
+ },
98
+ });
99
+
100
+ const handleSubmit = async (formData: FormData) => {
101
+ const name = formData.get("name") as string;
102
+ const email = formData.get("email") as string;
103
+
104
+ await execute({ name, email });
105
+ };
106
+
107
+ return (
108
+ <form action={handleSubmit}>
109
+ <input name="name" required />
110
+ <input name="email" type="email" required />
111
+ <button disabled={isLoading}>
112
+ {isLoading ? "Creating..." : "Create User"}
113
+ </button>
114
+ </form>
115
+ );
116
+ }
117
+ ```
118
+
119
+ ## 🔧 Advanced Features
120
+
121
+ ### Middleware System
122
+
123
+ ```typescript
124
+ const authClient = createActionClient()
125
+ .use(async ({ context, input }) => {
126
+ const session = await getSession();
127
+ if (!session) {
128
+ return {
129
+ error: {
130
+ code: "UNAUTHORIZED",
131
+ message: "You must be logged in",
132
+ },
133
+ };
134
+ }
135
+
136
+ return {
137
+ context: {
138
+ ...context,
139
+ userId: session.userId,
140
+ user: session.user,
141
+ },
142
+ };
143
+ });
144
+
145
+ // All actions created from authClient will have authentication
146
+ export const updateProfile = authClient
147
+ .inputSchema(profileSchema)
148
+ .action(async (input, context) => {
149
+ // context.userId is available here
150
+ return db.user.update({
151
+ where: { id: context.userId },
152
+ data: input,
153
+ });
154
+ });
155
+ ```
156
+
157
+ ### Form Actions
158
+
159
+ ```typescript
160
+ const contactSchema = z.object({
161
+ name: z.string(),
162
+ email: z.string().email(),
163
+ message: z.string().min(10),
164
+ });
165
+
166
+ export const submitContactForm = actionClient
167
+ .inputSchema(contactSchema)
168
+ .formAction(async ({ name, email, message }) => {
169
+ // Automatically parses FormData
170
+ await sendEmail({ to: email, subject: `Contact from ${name}`, body: message });
171
+ return { success: true };
172
+ });
173
+
174
+ // Use directly in form action
175
+ <form action={submitContactForm}>
176
+ <input name="name" required />
177
+ <input name="email" type="email" required />
178
+ <textarea name="message" required />
179
+ <button type="submit">Send</button>
180
+ </form>
181
+ ```
182
+
183
+ ### React Hook Form Integration
184
+
185
+ ```tsx
186
+ "use client";
187
+
188
+ import { useFormAction } from "next-action-forge/hooks";
189
+ import { updateProfileAction } from "@/app/actions/profile";
190
+ import { z } from "zod";
191
+
192
+ const profileSchema = z.object({
193
+ name: z.string().min(2, "Name must be at least 2 characters"),
194
+ bio: z.string().max(500).optional(),
195
+ website: z.string().url("Invalid URL").optional().or(z.literal("")),
196
+ });
197
+
198
+ export function ProfileForm({ user }: { user: User }) {
199
+ const { form, onSubmit, isSubmitting, actionState } = useFormAction({
200
+ action: updateProfileAction, // Must be created with .formAction()
201
+ schema: profileSchema,
202
+ defaultValues: {
203
+ name: user.name,
204
+ bio: user.bio || "",
205
+ website: user.website || "",
206
+ },
207
+ resetOnSuccess: false,
208
+ showSuccessToast: "Profile updated successfully!",
209
+ showErrorToast: true,
210
+ onSuccess: (updatedUser) => {
211
+ // Optionally redirect or update local state
212
+ console.log("Profile updated:", updatedUser);
213
+ },
214
+ });
215
+
216
+ return (
217
+ <form onSubmit={onSubmit} className="space-y-4">
218
+ <div>
219
+ <label htmlFor="name">Name</label>
220
+ <input
221
+ id="name"
222
+ {...form.register("name")}
223
+ className={form.formState.errors.name ? "error" : ""}
224
+ />
225
+ {form.formState.errors.name && (
226
+ <p className="error-message">{form.formState.errors.name.message}</p>
227
+ )}
228
+ </div>
229
+
230
+ <div>
231
+ <label htmlFor="bio">Bio</label>
232
+ <textarea
233
+ id="bio"
234
+ {...form.register("bio")}
235
+ rows={4}
236
+ placeholder="Tell us about yourself..."
237
+ />
238
+ {form.formState.errors.bio && (
239
+ <p className="error-message">{form.formState.errors.bio.message}</p>
240
+ )}
241
+ </div>
242
+
243
+ <div>
244
+ <label htmlFor="website">Website</label>
245
+ <input
246
+ id="website"
247
+ {...form.register("website")}
248
+ type="url"
249
+ placeholder="https://example.com"
250
+ />
251
+ {form.formState.errors.website && (
252
+ <p className="error-message">{form.formState.errors.website.message}</p>
253
+ )}
254
+ </div>
255
+
256
+ {/* Display global server errors */}
257
+ {form.formState.errors.root && (
258
+ <div className="alert alert-error">
259
+ {form.formState.errors.root.message}
260
+ </div>
261
+ )}
262
+
263
+ <button type="submit" disabled={isSubmitting}>
264
+ {isSubmitting ? "Saving..." : "Save Profile"}
265
+ </button>
266
+ </form>
267
+ );
268
+ }
269
+
270
+ // The server action must be created with .formAction()
271
+ // app/actions/profile.ts
272
+ export const updateProfileAction = actionClient
273
+ .inputSchema(profileSchema)
274
+ .formAction(async ({ name, bio, website }, context) => {
275
+ const updatedUser = await db.user.update({
276
+ where: { id: context.userId },
277
+ data: { name, bio, website },
278
+ });
279
+
280
+ return updatedUser;
281
+ });
282
+ ```
283
+
284
+ ### Custom Error Classes
285
+
286
+ ```typescript
287
+ // Define your error class with toServerActionError method
288
+ class ValidationError extends Error {
289
+ constructor(public field: string, message: string) {
290
+ super(message);
291
+ }
292
+
293
+ toServerActionError() {
294
+ return {
295
+ code: "VALIDATION_ERROR",
296
+ message: this.message,
297
+ field: this.field,
298
+ };
299
+ }
300
+ }
301
+
302
+ // The error will be automatically transformed
303
+ export const updateUser = actionClient
304
+ .inputSchema(userSchema)
305
+ .action(async (input) => {
306
+ if (await isEmailTaken(input.email)) {
307
+ throw new ValidationError("email", "Email is already taken");
308
+ }
309
+
310
+ // ... rest of the logic
311
+ });
312
+ ```
313
+
314
+ ### Error Handler Adapter
315
+
316
+ ```typescript
317
+ // Create a custom error adapter for your error library
318
+ export function createErrorAdapter() {
319
+ return (error: unknown): ServerActionError | undefined => {
320
+ if (error instanceof MyCustomError) {
321
+ return {
322
+ code: error.code,
323
+ message: error.message,
324
+ statusCode: error.statusCode,
325
+ };
326
+ }
327
+
328
+ // Return undefined to use default error handling
329
+ return undefined;
330
+ };
331
+ }
332
+
333
+ // Use it globally
334
+ const actionClient = createActionClient()
335
+ .onError(createErrorAdapter());
336
+ ```
337
+
338
+ ### Optimistic Updates
339
+
340
+ ```tsx
341
+ import { useOptimisticAction } from "next-action-forge/hooks";
342
+
343
+ function TodoList({ todos }: { todos: Todo[] }) {
344
+ const { optimisticData, execute } = useOptimisticAction(
345
+ todos,
346
+ toggleTodo,
347
+ {
348
+ updateFn: (currentTodos, { id }) => {
349
+ return currentTodos.map(todo =>
350
+ todo.id === id ? { ...todo, done: !todo.done } : todo
351
+ );
352
+ },
353
+ }
354
+ );
355
+
356
+ return (
357
+ <ul>
358
+ {optimisticData.map(todo => (
359
+ <li key={todo.id}>
360
+ <input
361
+ type="checkbox"
362
+ checked={todo.done}
363
+ onChange={() => execute({ id: todo.id })}
364
+ />
365
+ {todo.title}
366
+ </li>
367
+ ))}
368
+ </ul>
369
+ );
370
+ }
371
+ ```
372
+
373
+ ## 📚 API Reference
374
+
375
+ ### ServerActionClient
376
+
377
+ The main class for creating type-safe server actions with method chaining.
378
+
379
+ ```typescript
380
+ const client = createActionClient();
381
+
382
+ // Available methods:
383
+ client
384
+ .use(middleware) // Add middleware
385
+ .inputSchema(zodSchema) // Set input validation schema
386
+ .outputSchema(zodSchema) // Set output validation schema
387
+ .onError(handler) // Set error handler
388
+ .action(serverFunction) // Define the server action
389
+ .formAction(serverFunction) // Define a form action
390
+
391
+ // You can also create a pre-configured client with default error handling:
392
+ const clientWithErrorHandler = createActionClient()
393
+ .onError((error) => {
394
+ console.error("Action error:", error);
395
+ return {
396
+ code: "INTERNAL_ERROR",
397
+ message: "Something went wrong",
398
+ };
399
+ });
400
+
401
+ // All actions created from this client will use the error handler
402
+ const myAction = clientWithErrorHandler
403
+ .inputSchema(schema)
404
+ .action(async (input) => {
405
+ // Your logic here
406
+ });
407
+ ```
408
+
409
+ ### Hooks
410
+
411
+ - `useServerAction` - Execute server actions with loading state and callbacks
412
+ - `useOptimisticAction` - Optimistic UI updates
413
+ - `useFormAction` - Integration with React Hook Form (works with `.formAction()` or form-compatible actions)
414
+
415
+ ### Error Handling
416
+
417
+ The library follows a precedence order for error handling:
418
+
419
+ 1. Custom error handler (if provided via `onError`)
420
+ 2. Duck-typed errors (objects with `toServerActionError()` method)
421
+ 3. Zod validation errors (automatically formatted)
422
+ 4. Generic errors (with safe error messages in production)
423
+
424
+ ## 📄 License
425
+
426
+ MIT
427
+
428
+ ## 🤝 Contributing
429
+
430
+ Contributions are welcome! Please feel free to submit a Pull Request.
431
+
432
+ ## 🙏 Acknowledgments
433
+
434
+ Inspired by [next-safe-action](https://github.com/TheEdoRan/next-safe-action) but with a simpler, more lightweight approach.
@@ -0,0 +1,3 @@
1
+ export { ServerActionClient, createActionClient, handleServerActionError, isErrorResponse } from '../index.mjs';
2
+ import '../index-BJx3beLJ.mjs';
3
+ import 'zod';
@@ -0,0 +1,3 @@
1
+ export { ServerActionClient, createActionClient, handleServerActionError, isErrorResponse } from '../index.js';
2
+ import '../index-BJx3beLJ.js';
3
+ import 'zod';