Skip to content

Commit 714554d

Browse files
authoredApr 3, 2024··
feat: support middleware chaining (#89)
See #88
1 parent 9e38c05 commit 714554d

37 files changed

+1037
-577
lines changed
 

‎README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ https://github.com/TheEdoRan/next-safe-action/assets/1337629/7ebc398e-6c7d-49b2-
1616

1717
- ✅ Pretty simple
1818
- ✅ End-to-end type safety
19-
-Context based clients (with middlewares)
19+
-Powerful middleware system
2020
- ✅ Input validation using multiple validation libraries
2121
- ✅ Advanced server error handling
2222
- ✅ Optimistic updates

‎package-lock.json

+1-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/example-app/src/app/(examples)/direct/login-action.ts

+21-18
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,32 @@ import { action } from "@/lib/safe-action";
44
import { returnValidationErrors } from "next-safe-action";
55
import { z } from "zod";
66

7-
const input = z.object({
7+
const schema = z.object({
88
username: z.string().min(3).max(10),
99
password: z.string().min(8).max(100),
1010
});
1111

12-
export const loginUser = action(input, async ({ username, password }, ctx) => {
13-
if (username === "johndoe") {
14-
returnValidationErrors(input, {
12+
export const loginUser = action
13+
.metadata({ actionName: "loginUser" })
14+
.schema(schema)
15+
.define(async ({ username, password }, ctx) => {
16+
if (username === "johndoe") {
17+
returnValidationErrors(schema, {
18+
username: {
19+
_errors: ["user_suspended"],
20+
},
21+
});
22+
}
23+
24+
if (username === "user" && password === "password") {
25+
return {
26+
success: true,
27+
};
28+
}
29+
30+
returnValidationErrors(schema, {
1531
username: {
16-
_errors: ["user_suspended"],
32+
_errors: ["incorrect_credentials"],
1733
},
1834
});
19-
}
20-
21-
if (username === "user" && password === "password") {
22-
return {
23-
success: true,
24-
};
25-
}
26-
27-
returnValidationErrors(input, {
28-
username: {
29-
_errors: ["incorrect_credentials"],
30-
},
3135
});
32-
});

‎packages/example-app/src/app/(examples)/hook/deleteuser-action.ts

+13-10
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,21 @@
33
import { ActionError, action } from "@/lib/safe-action";
44
import { z } from "zod";
55

6-
const input = z.object({
6+
const schema = z.object({
77
userId: z.string().min(1).max(10),
88
});
99

10-
export const deleteUser = action(input, async ({ userId }) => {
11-
await new Promise((res) => setTimeout(res, 1000));
10+
export const deleteUser = action
11+
.metadata({ actionName: "deleteUser" })
12+
.schema(schema)
13+
.define(async ({ userId }) => {
14+
await new Promise((res) => setTimeout(res, 1000));
1215

13-
if (Math.random() > 0.5) {
14-
throw new ActionError("Could not delete user!");
15-
}
16+
if (Math.random() > 0.5) {
17+
throw new ActionError("Could not delete user!");
18+
}
1619

17-
return {
18-
deletedUserId: userId,
19-
};
20-
});
20+
return {
21+
deletedUserId: userId,
22+
};
23+
});

‎packages/example-app/src/app/(examples)/nested-schema/shop-action.ts

+9-6
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { action } from "@/lib/safe-action";
44
import { z } from "zod";
55

6-
const input = z
6+
const schema = z
77
.object({
88
user: z.object({
99
id: z.string().uuid(),
@@ -68,8 +68,11 @@ const input = z
6868
}
6969
});
7070

71-
export const buyProduct = action(input, async () => {
72-
return {
73-
success: true,
74-
};
75-
});
71+
export const buyProduct = action
72+
.metadata({ actionName: "buyProduct" })
73+
.schema(schema)
74+
.define(async () => {
75+
return {
76+
success: true,
77+
};
78+
});

‎packages/example-app/src/app/(examples)/optimistic-hook/addlikes-action.ts

+14-11
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,23 @@ const incrementLikes = (by: number) => {
1313
return likes;
1414
};
1515

16-
const input = z.object({
16+
const schema = z.object({
1717
incrementBy: z.number(),
1818
});
1919

20-
export const addLikes = action(input, async ({ incrementBy }) => {
21-
await new Promise((res) => setTimeout(res, 2000));
20+
export const addLikes = action
21+
.metadata({ actionName: "addLikes" })
22+
.schema(schema)
23+
.define(async ({ incrementBy }) => {
24+
await new Promise((res) => setTimeout(res, 2000));
2225

23-
const likesCount = incrementLikes(incrementBy);
26+
const likesCount = incrementLikes(incrementBy);
2427

25-
// This Next.js function revalidates the provided path.
26-
// More info here: https://nextjs.org/docs/app/api-reference/functions/revalidatePath
27-
revalidatePath("/optimistic-hook");
28+
// This Next.js function revalidates the provided path.
29+
// More info here: https://nextjs.org/docs/app/api-reference/functions/revalidatePath
30+
revalidatePath("/optimistic-hook");
2831

29-
return {
30-
likesCount,
31-
};
32-
});
32+
return {
33+
likesCount,
34+
};
35+
});

‎packages/example-app/src/app/(examples)/react-hook-form/buyproduct-action.ts

+10-7
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ import { action } from "@/lib/safe-action";
44
import { randomUUID } from "crypto";
55
import { schema } from "./validation";
66

7-
export const buyProduct = action(schema, async ({ productId }) => {
8-
return {
9-
productId,
10-
transactionId: randomUUID(),
11-
transactionTimestamp: Date.now(),
12-
};
13-
});
7+
export const buyProduct = action
8+
.metadata({ actionName: "buyProduct" })
9+
.schema(schema)
10+
.define(async ({ productId }) => {
11+
return {
12+
productId,
13+
transactionId: randomUUID(),
14+
transactionTimestamp: Date.now(),
15+
};
16+
});

‎packages/example-app/src/app/(examples)/server-form/signup-action.ts

+9-6
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@ const schema = zfd.formData({
99
password: zfd.text(z.string().min(8)),
1010
});
1111

12-
export const signup = action(schema, async ({ email, password }) => {
13-
console.log("Email:", email, "Password:", password);
14-
return {
15-
success: true,
16-
};
17-
});
12+
export const signup = action
13+
.metadata({ actionName: "signup" })
14+
.schema(schema)
15+
.define(async ({ email, password }) => {
16+
console.log("Email:", email, "Password:", password);
17+
return {
18+
success: true,
19+
};
20+
});

‎packages/example-app/src/app/(examples)/with-context/edituser-action.ts

+31-28
Original file line numberDiff line numberDiff line change
@@ -3,41 +3,44 @@
33
import { authAction } from "@/lib/safe-action";
44
import { maxLength, minLength, object, string } from "valibot";
55

6-
const input = object({
6+
const schema = object({
77
fullName: string([minLength(3, "Too short"), maxLength(20, "Too long")]),
88
age: string([minLength(2, "Too young"), maxLength(3, "Too old")]),
99
});
1010

11-
export const editUser = authAction(
12-
input,
13-
// Here you have access to `userId`, which comes from `buildContext`
14-
// return object in src/lib/safe-action.ts.
15-
// \\\\\
16-
async ({ fullName, age }, { userId }) => {
17-
if (fullName.toLowerCase() === "john doe") {
18-
return {
19-
error: {
20-
cause: "forbidden_name",
21-
},
22-
};
23-
}
11+
export const editUser = authAction
12+
.metadata({ actionName: "editUser" })
13+
.schema(schema)
14+
.define(
15+
// Here you have access to `userId`, and `sessionId which comes from middleware functions
16+
// defined before.
17+
// \\\\\\\\\\\\\\\\\\
18+
async ({ fullName, age }, { ctx: { userId, sessionId } }) => {
19+
if (fullName.toLowerCase() === "john doe") {
20+
return {
21+
error: {
22+
cause: "forbidden_name",
23+
},
24+
};
25+
}
26+
27+
const intAge = parseInt(age);
2428

25-
const intAge = parseInt(age);
29+
if (Number.isNaN(intAge)) {
30+
return {
31+
error: {
32+
reason: "invalid_age", // different key in `error`, will be correctly inferred
33+
},
34+
};
35+
}
2636

27-
if (Number.isNaN(intAge)) {
2837
return {
29-
error: {
30-
reason: "invalid_age", // different key in `error`, will be correctly inferred
38+
success: {
39+
newFullName: fullName,
40+
newAge: intAge,
41+
userId,
42+
sessionId,
3143
},
3244
};
3345
}
34-
35-
return {
36-
success: {
37-
newFullName: fullName,
38-
newAge: intAge,
39-
userId,
40-
},
41-
};
42-
}
43-
);
46+
);

‎packages/example-app/src/lib/safe-action.ts

+69-24
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,8 @@ import { DEFAULT_SERVER_ERROR, createSafeActionClient } from "next-safe-action";
33

44
export class ActionError extends Error {}
55

6-
const handleReturnedServerError = (e: Error) => {
7-
// If the error is an instance of `ActionError`, unmask the message.
8-
if (e instanceof ActionError) {
9-
return e.message;
10-
}
11-
12-
// Otherwise return default error message.
13-
return DEFAULT_SERVER_ERROR;
14-
};
15-
166
export const action = createSafeActionClient({
17-
// You can provide a custom log Promise, otherwise the lib will use `console.error`
7+
// You can provide a custom logging function, otherwise the lib will use `console.error`
188
// as the default logging system. If you want to disable server errors logging,
199
// just pass an empty Promise.
2010
handleServerErrorLog: (e) => {
@@ -23,23 +13,78 @@ export const action = createSafeActionClient({
2313
e.message
2414
);
2515
},
26-
handleReturnedServerError,
16+
handleReturnedServerError: (e) => {
17+
// If the error is an instance of `ActionError`, unmask the message.
18+
if (e instanceof ActionError) {
19+
return e.message;
20+
}
21+
22+
// Otherwise return default error message.
23+
return DEFAULT_SERVER_ERROR;
24+
},
25+
}).use(async ({ next, metadata }) => {
26+
// Here we use a logging middleware.
27+
const start = Date.now();
28+
29+
// Here we await the next middleware.
30+
const result = await next({ ctx: null });
31+
32+
const end = Date.now();
33+
34+
// Log the execution time of the action.
35+
console.log(
36+
"LOGGING MIDDLEWARE: this action took",
37+
end - start,
38+
"ms to execute"
39+
);
40+
41+
// Log the result
42+
console.log("LOGGING MIDDLEWARE: result ->", result);
43+
44+
// Log metadata
45+
console.log("LOGGING MIDDLEWARE: metadata ->", metadata);
46+
47+
// And then return the result of the awaited next middleware.
48+
return result;
2749
});
2850

29-
export const authAction = createSafeActionClient({
30-
// You can provide a middleware function. In this case, context is used
31-
// for (fake) auth purposes.
32-
middleware(parsedInput) {
51+
async function getSessionId() {
52+
return randomUUID();
53+
}
54+
55+
export const authAction = action
56+
// Clone the base client to extend this one with additional middleware functions.
57+
.clone()
58+
// In this case, context is used for (fake) auth purposes.
59+
.use(async ({ next }) => {
3360
const userId = randomUUID();
3461

62+
console.log("HELLO FROM FIRST AUTH ACTION MIDDLEWARE, USER ID:", userId);
63+
64+
return next({
65+
ctx: {
66+
userId,
67+
},
68+
});
69+
})
70+
// Here we get `userId` from the previous context, and it's all type safe.
71+
.use(async ({ ctx, next }) => {
72+
// Emulate a slow server.
73+
await new Promise((res) =>
74+
setTimeout(res, Math.max(Math.random() * 2000, 500))
75+
);
76+
77+
const sessionId = await getSessionId();
78+
3579
console.log(
36-
"HELLO FROM ACTION MIDDLEWARE, USER ID:",
37-
userId,
38-
"PARSED INPUT:",
39-
parsedInput
80+
"HELLO FROM SECOND AUTH ACTION MIDDLEWARE, SESSION ID:",
81+
sessionId
4082
);
4183

42-
return { userId };
43-
},
44-
handleReturnedServerError,
45-
});
84+
return next({
85+
ctx: {
86+
...ctx, // here we spread the previous context to extend it
87+
sessionId, // with session id
88+
},
89+
});
90+
});

‎packages/next-safe-action/.eslintrc.js

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ module.exports = {
1717
"@typescript-eslint/no-floating-promises": "warn",
1818
"@typescript-eslint/ban-ts-comment": "off",
1919
"@typescript-eslint/ban-types": "off",
20+
"@typescript-eslint/no-this-alias": "off",
2021
"no-mixed-spaces-and-tabs": "off",
2122
"react-hooks/exhaustive-deps": "warn",
2223
},

‎packages/next-safe-action/src/hooks.ts

+12-42
Original file line numberDiff line numberDiff line change
@@ -5,42 +5,10 @@ import { isNotFoundError } from "next/dist/client/components/not-found.js";
55
import { isRedirectError } from "next/dist/client/components/redirect.js";
66
import * as React from "react";
77
import {} from "react/experimental";
8-
import type { SafeAction } from ".";
9-
import type { MaybePromise } from "./utils";
8+
import type { HookActionStatus, HookCallbacks, HookResult } from "./hooks.types";
9+
import type { SafeActionFn } from "./index.types";
1010
import { isError } from "./utils";
1111

12-
// TYPES
13-
14-
/**
15-
* Type of `result` object returned by `useAction` and `useOptimisticAction` hooks.
16-
*/
17-
export type HookResult<S extends Schema, Data> = Awaited<ReturnType<SafeAction<S, Data>>> & {
18-
fetchError?: string;
19-
};
20-
21-
/**
22-
* Type of hooks callbacks. These are executed when action is in a specific state.
23-
*/
24-
export type HookCallbacks<S extends Schema, Data> = {
25-
onExecute?: (input: InferIn<S>) => MaybePromise<void>;
26-
onSuccess?: (data: Data, input: InferIn<S>, reset: () => void) => MaybePromise<void>;
27-
onError?: (
28-
error: Omit<HookResult<S, Data>, "data">,
29-
input: InferIn<S>,
30-
reset: () => void
31-
) => MaybePromise<void>;
32-
onSettled?: (
33-
result: HookResult<S, Data>,
34-
input: InferIn<S>,
35-
reset: () => void
36-
) => MaybePromise<void>;
37-
};
38-
39-
/**
40-
* Type of the action status returned by `useAction` and `useOptimisticAction` hooks.
41-
*/
42-
export type HookActionStatus = "idle" | "executing" | "hasSucceeded" | "hasErrored";
43-
4412
// UTILS
4513

4614
const DEFAULT_RESULT = {
@@ -112,13 +80,13 @@ const useActionCallbacks = <const S extends Schema, const Data>(
11280

11381
/**
11482
* Use the action from a Client Component via hook.
115-
* @param safeAction The typesafe action.
83+
* @param safeActionFn The typesafe action.
11684
* @param callbacks Optional callbacks executed based on the action status.
11785
*
11886
* {@link https://next-safe-action.dev/docs/usage/client-components/hooks/useaction See an example}
11987
*/
12088
export const useAction = <const S extends Schema, const Data>(
121-
safeAction: SafeAction<S, Data>,
89+
safeActionFn: SafeActionFn<S, Data>,
12290
callbacks?: HookCallbacks<S, Data>
12391
) => {
12492
const [, startTransition] = React.useTransition();
@@ -134,7 +102,7 @@ export const useAction = <const S extends Schema, const Data>(
134102
setIsExecuting(true);
135103

136104
return startTransition(() => {
137-
return safeAction(input)
105+
return safeActionFn(input)
138106
.then((res) => setResult(res ?? DEFAULT_RESULT))
139107
.catch((e) => {
140108
if (isRedirectError(e) || isNotFoundError(e)) {
@@ -148,7 +116,7 @@ export const useAction = <const S extends Schema, const Data>(
148116
});
149117
});
150118
},
151-
[safeAction]
119+
[safeActionFn]
152120
);
153121

154122
const reset = React.useCallback(() => {
@@ -169,15 +137,15 @@ export const useAction = <const S extends Schema, const Data>(
169137
* Use the action from a Client Component via hook, with optimistic data update.
170138
*
171139
* **NOTE: This hook uses an experimental React feature.**
172-
* @param safeAction The typesafe action.
140+
* @param safeActionFn The typesafe action.
173141
* @param initialOptimisticData Initial optimistic data.
174142
* @param reducer Optimistic state reducer.
175143
* @param callbacks Optional callbacks executed based on the action status.
176144
*
177145
* {@link https://next-safe-action.dev/docs/usage/client-components/hooks/useoptimisticaction See an example}
178146
*/
179147
export const useOptimisticAction = <const S extends Schema, const Data>(
180-
safeAction: SafeAction<S, Data>,
148+
safeActionFn: SafeActionFn<S, Data>,
181149
initialOptimisticData: Data,
182150
reducer: (state: Data, input: InferIn<S>) => Data,
183151
callbacks?: HookCallbacks<S, Data>
@@ -201,7 +169,7 @@ export const useOptimisticAction = <const S extends Schema, const Data>(
201169

202170
return startTransition(() => {
203171
setOptimisticState(input);
204-
return safeAction(input)
172+
return safeActionFn(input)
205173
.then((res) => setResult(res ?? DEFAULT_RESULT))
206174
.catch((e) => {
207175
if (isRedirectError(e) || isNotFoundError(e)) {
@@ -215,7 +183,7 @@ export const useOptimisticAction = <const S extends Schema, const Data>(
215183
});
216184
});
217185
},
218-
[setOptimisticState, safeAction]
186+
[setOptimisticState, safeActionFn]
219187
);
220188

221189
const reset = React.useCallback(() => {
@@ -232,3 +200,5 @@ export const useOptimisticAction = <const S extends Schema, const Data>(
232200
status,
233201
};
234202
};
203+
204+
export type { HookActionStatus, HookCallbacks, HookResult };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { InferIn, Schema } from "@typeschema/main";
2+
import type { SafeActionResult } from "./index.types";
3+
import type { MaybePromise } from "./utils";
4+
5+
/**
6+
* Type of `result` object returned by `useAction` and `useOptimisticAction` hooks.
7+
*/
8+
export type HookResult<S extends Schema, Data> = SafeActionResult<S, Data> & {
9+
fetchError?: string;
10+
};
11+
12+
/**
13+
* Type of hooks callbacks. These are executed when action is in a specific state.
14+
*/
15+
export type HookCallbacks<S extends Schema, Data> = {
16+
onExecute?: (input: InferIn<S>) => MaybePromise<void>;
17+
onSuccess?: (data: Data, input: InferIn<S>, reset: () => void) => MaybePromise<void>;
18+
onError?: (
19+
error: Omit<HookResult<S, Data>, "data">,
20+
input: InferIn<S>,
21+
reset: () => void
22+
) => MaybePromise<void>;
23+
onSettled?: (
24+
result: HookResult<S, Data>,
25+
input: InferIn<S>,
26+
reset: () => void
27+
) => MaybePromise<void>;
28+
};
29+
30+
/**
31+
* Type of the action status returned by `useAction` and `useOptimisticAction` hooks.
32+
*/
33+
export type HookActionStatus = "idle" | "executing" | "hasSucceeded" | "hasErrored";
+198-124
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,196 @@
1-
import type { Infer, InferIn, Schema } from "@typeschema/main";
1+
import type { Schema } from "@typeschema/main";
22
import { validate } from "@typeschema/main";
33
import { isNotFoundError } from "next/dist/client/components/not-found.js";
44
import { isRedirectError } from "next/dist/client/components/redirect.js";
5-
import type { ErrorList, Extend, MaybePromise, SchemaErrors } from "./utils";
6-
import { buildValidationErrors, isError } from "./utils";
5+
import type {
6+
ActionMetadata,
7+
MiddlewareFn,
8+
MiddlewareResult,
9+
SafeActionClientOpts,
10+
SafeActionFn,
11+
SafeActionResult,
12+
ServerCodeFn,
13+
} from "./index.types";
14+
import { DEFAULT_SERVER_ERROR, isError } from "./utils";
15+
import {
16+
ServerValidationError,
17+
buildValidationErrors,
18+
returnValidationErrors,
19+
} from "./validation-errors";
20+
import type { ValidationErrors } from "./validation-errors.types";
21+
22+
class SafeActionClient<const Ctx = null> {
23+
private readonly handleServerErrorLog: NonNullable<SafeActionClientOpts["handleServerErrorLog"]>;
24+
private readonly handleReturnedServerError: NonNullable<
25+
SafeActionClientOpts["handleReturnedServerError"]
26+
>;
27+
28+
private middlewareFns: MiddlewareFn<any, any, any>[];
29+
private _metadata: ActionMetadata = {};
30+
31+
constructor(
32+
opts: { middlewareFns: MiddlewareFn<any, any, any>[] } & Required<SafeActionClientOpts>
33+
) {
34+
this.middlewareFns = opts.middlewareFns;
35+
this.handleServerErrorLog = opts.handleServerErrorLog;
36+
this.handleReturnedServerError = opts.handleReturnedServerError;
37+
}
738

8-
// TYPES
39+
/**
40+
* Clone the safe action client keeping the same middleware and initialization functions.
41+
* This is used to extend the base client with additional middleware functions.
42+
* @returns {SafeActionClient}
43+
*/
44+
public clone() {
45+
return new SafeActionClient<Ctx>({
46+
handleReturnedServerError: this.handleReturnedServerError,
47+
handleServerErrorLog: this.handleServerErrorLog,
48+
middlewareFns: [...this.middlewareFns], // copy the middleware stack so we don't mutate it
49+
});
50+
}
951

10-
/**
11-
* Type of options when creating a new safe action client.
12-
*/
13-
export type SafeClientOpts<Context, MiddlewareData> = {
14-
handleServerErrorLog?: (e: Error) => MaybePromise<void>;
15-
handleReturnedServerError?: (e: Error) => MaybePromise<string>;
16-
middleware?: (parsedInput: any, data?: MiddlewareData) => MaybePromise<Context>;
17-
};
52+
/**
53+
* Use a middleware function.
54+
* @param middlewareFn Middleware function
55+
* @returns SafeActionClient
56+
*/
57+
public use<const ClientInput, const NextCtx>(
58+
middlewareFn: MiddlewareFn<ClientInput, Ctx, NextCtx>
59+
) {
60+
this.middlewareFns.push(middlewareFn);
61+
62+
return new SafeActionClient<NextCtx>({
63+
middlewareFns: this.middlewareFns,
64+
handleReturnedServerError: this.handleReturnedServerError,
65+
handleServerErrorLog: this.handleServerErrorLog,
66+
});
67+
}
1868

19-
/**
20-
* Type of the function called from Client Components with typesafe input data.
21-
*/
22-
export type SafeAction<S extends Schema, Data> = (input: InferIn<S>) => Promise<{
23-
data?: Data;
24-
serverError?: string;
25-
validationErrors?: ValidationErrors<S>;
26-
}>;
69+
/**
70+
* Set metadata for the action that will be defined afterwards.
71+
* @param data Metadata for the action
72+
* @returns {Function} Define a new action
73+
*/
74+
public metadata(data: ActionMetadata) {
75+
this._metadata = data;
2776

28-
/**
29-
* Type of the function that executes server code when defining a new safe action.
30-
*/
31-
export type ServerCodeFn<S extends Schema, Data, Context> = (
32-
parsedInput: Infer<S>,
33-
ctx: Context
34-
) => Promise<Data>;
77+
return {
78+
schema: this.schema.bind(this),
79+
};
80+
}
3581

36-
/**
37-
* Type of the returned object when input validation fails.
38-
*/
39-
export type ValidationErrors<S extends Schema> = Extend<ErrorList & SchemaErrors<Infer<S>>>;
82+
/**
83+
* Pass an input schema to define safe action arguments.
84+
* @param schema An input schema supported by [TypeSchema](https://typeschema.com/#coverage).
85+
* @returns {Function} The `define` function, which is used to define a new safe action.
86+
*/
87+
public schema<const S extends Schema>(schema: S) {
88+
const classThis = this;
89+
90+
return {
91+
/**
92+
* Define a new safe action.
93+
* @param serverCodeFn A function that executes the server code.
94+
* @returns {SafeActionFn}
95+
*/
96+
define<const Data = null>(serverCodeFn: ServerCodeFn<S, Data, Ctx>): SafeActionFn<S, Data> {
97+
return async (clientInput: unknown) => {
98+
let prevCtx: any = null;
99+
let frameworkError: Error | undefined = undefined;
100+
const middlewareResult: MiddlewareResult<any> = { success: false };
101+
102+
// Execute the middleware stack.
103+
const executeMiddlewareChain = async (idx = 0) => {
104+
const currentFn = classThis.middlewareFns[idx];
105+
106+
middlewareResult.ctx = prevCtx;
107+
108+
try {
109+
if (currentFn) {
110+
await currentFn({
111+
clientInput, // pass raw client input
112+
ctx: prevCtx,
113+
metadata: classThis._metadata,
114+
next: async ({ ctx }) => {
115+
prevCtx = ctx;
116+
await executeMiddlewareChain(idx + 1);
117+
return middlewareResult;
118+
},
119+
});
120+
} else {
121+
const parsedInput = await validate(schema, clientInput);
122+
123+
if (!parsedInput.success) {
124+
middlewareResult.validationErrors = buildValidationErrors<S>(parsedInput.issues);
125+
return;
126+
}
127+
128+
const data =
129+
(await serverCodeFn(parsedInput.data, {
130+
ctx: prevCtx,
131+
metadata: classThis._metadata,
132+
})) ?? null;
133+
middlewareResult.success = true;
134+
middlewareResult.data = data;
135+
middlewareResult.parsedInput = parsedInput.data;
136+
}
137+
} catch (e: unknown) {
138+
// next/navigation functions work by throwing an error that will be
139+
// processed internally by Next.js.
140+
if (isRedirectError(e) || isNotFoundError(e)) {
141+
middlewareResult.success = true;
142+
frameworkError = e;
143+
return;
144+
}
145+
146+
// If error is ServerValidationError, return validationErrors as if schema validation would fail.
147+
if (e instanceof ServerValidationError) {
148+
middlewareResult.validationErrors = e.validationErrors;
149+
return;
150+
}
151+
152+
if (!isError(e)) {
153+
console.warn("Could not handle server error. Not an instance of Error: ", e);
154+
middlewareResult.serverError = DEFAULT_SERVER_ERROR;
155+
return;
156+
}
157+
158+
await Promise.resolve(classThis.handleServerErrorLog(e));
159+
160+
middlewareResult.serverError = await Promise.resolve(
161+
classThis.handleReturnedServerError(e)
162+
);
163+
}
164+
};
165+
166+
await executeMiddlewareChain();
40167

41-
// UTILS
168+
// If an internal framework error occurred, throw it, so it will be processed by Next.js.
169+
if (frameworkError) {
170+
throw frameworkError;
171+
}
42172

43-
export const DEFAULT_SERVER_ERROR = "Something went wrong while executing the operation";
173+
const actionResult: SafeActionResult<S, Data> = {};
44174

45-
// SAFE ACTION CLIENT
175+
if (typeof middlewareResult.data !== "undefined") {
176+
actionResult.data = middlewareResult.data as Data;
177+
}
178+
179+
if (typeof middlewareResult.validationErrors !== "undefined") {
180+
actionResult.validationErrors =
181+
middlewareResult.validationErrors as ValidationErrors<S>;
182+
}
183+
184+
if (typeof middlewareResult.serverError !== "undefined") {
185+
actionResult.serverError = middlewareResult.serverError;
186+
}
187+
188+
return actionResult;
189+
};
190+
},
191+
};
192+
}
193+
}
46194

47195
/**
48196
* Initialize a new action client.
@@ -51,9 +199,7 @@ export const DEFAULT_SERVER_ERROR = "Something went wrong while executing the op
51199
*
52200
* {@link https://next-safe-action.dev/docs/getting-started See an example}
53201
*/
54-
export const createSafeActionClient = <Context, MiddlewareData>(
55-
createOpts?: SafeClientOpts<Context, MiddlewareData>
56-
) => {
202+
export const createSafeActionClient = (createOpts?: SafeActionClientOpts) => {
57203
// If server log function is not provided, default to `console.error` for logging
58204
// server error messages.
59205
const handleServerErrorLog =
@@ -68,93 +214,21 @@ export const createSafeActionClient = <Context, MiddlewareData>(
68214
const handleReturnedServerError = (e: Error) =>
69215
createOpts?.handleReturnedServerError?.(e) || DEFAULT_SERVER_ERROR;
70216

71-
// `actionBuilder` is the server function that creates a new action.
72-
// It expects an input schema and a `serverCode` function, so the action
73-
// knows what to do on the server when called by the client.
74-
// It returns a function callable by the client.
75-
const actionBuilder = <const S extends Schema, const Data>(
76-
schema: S,
77-
serverCode: ServerCodeFn<S, Data, Context>,
78-
utils?: {
79-
middlewareData?: MiddlewareData;
80-
}
81-
): SafeAction<S, Data> => {
82-
// This is the function called by client. If `input` fails the schema
83-
// parsing, the function will return a `validationErrors` object, containing
84-
// all the invalid fields provided.
85-
return async (clientInput) => {
86-
try {
87-
const parsedInput = await validate(schema, clientInput);
88-
89-
// If schema validation fails.
90-
if (!parsedInput.success) {
91-
return {
92-
validationErrors: buildValidationErrors<S>(parsedInput.issues),
93-
};
94-
}
95-
96-
// Get the context if `middleware` is provided.
97-
const ctx = (await Promise.resolve(
98-
createOpts?.middleware?.(parsedInput.data, utils?.middlewareData)
99-
)) as Context;
100-
101-
// Get `result.data` from the server code function. If it doesn't return
102-
// anything, `data` will be `null`.
103-
const data = ((await serverCode(parsedInput.data, ctx)) ?? null) as Data;
104-
105-
return { data };
106-
} catch (e: unknown) {
107-
// next/navigation functions work by throwing an error that will be
108-
// processed internally by Next.js. So, in this case we need to rethrow it.
109-
if (isRedirectError(e) || isNotFoundError(e)) {
110-
throw e;
111-
}
112-
113-
// If error is ServerValidationError, return validationErrors as if schema validation would fail.
114-
if (e instanceof ServerValidationError) {
115-
return { validationErrors: e.validationErrors as ValidationErrors<S> };
116-
}
117-
118-
// If error cannot be handled, warn the user and return a generic message.
119-
if (!isError(e)) {
120-
console.warn("Could not handle server error. Not an instance of Error: ", e);
121-
return { serverError: DEFAULT_SERVER_ERROR };
122-
}
123-
124-
await Promise.resolve(handleServerErrorLog(e));
125-
126-
return {
127-
serverError: await Promise.resolve(handleReturnedServerError(e)),
128-
};
129-
}
130-
};
131-
};
132-
133-
return actionBuilder;
217+
return new SafeActionClient({
218+
middlewareFns: [async ({ next }) => next({ ctx: null })],
219+
handleServerErrorLog,
220+
handleReturnedServerError,
221+
});
134222
};
135223

136-
// VALIDATION ERRORS
137-
138-
// This class is internally used to throw validation errors in action's server code function, using
139-
// `returnValidationErrors`.
140-
class ServerValidationError<S extends Schema> extends Error {
141-
public validationErrors: ValidationErrors<S>;
142-
constructor(validationErrors: ValidationErrors<S>) {
143-
super("Server Validation Error");
144-
this.validationErrors = validationErrors;
145-
}
146-
}
224+
export { DEFAULT_SERVER_ERROR, returnValidationErrors, type ValidationErrors };
147225

148-
/**
149-
* Return custom validation errors to the client from the action's server code function.
150-
* Code declared after this function invocation will not be executed.
151-
* @param schema Input schema
152-
* @param validationErrors Validation errors object
153-
* @throws {ServerValidationError}
154-
*/
155-
export function returnValidationErrors<S extends Schema>(
156-
schema: S,
157-
validationErrors: ValidationErrors<S>
158-
): never {
159-
throw new ServerValidationError<S>(validationErrors);
160-
}
226+
export type {
227+
ActionMetadata,
228+
MiddlewareFn,
229+
MiddlewareResult,
230+
SafeActionClientOpts,
231+
SafeActionFn,
232+
SafeActionResult,
233+
ServerCodeFn,
234+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import type { Infer, InferIn, Schema } from "@typeschema/main";
2+
import type { MaybePromise } from "./utils";
3+
import type { ValidationErrors } from "./validation-errors.types";
4+
5+
/**
6+
* Type of options when creating a new safe action client.
7+
*/
8+
export type SafeActionClientOpts = {
9+
handleServerErrorLog?: (e: Error) => MaybePromise<void>;
10+
handleReturnedServerError?: (e: Error) => MaybePromise<string>;
11+
};
12+
13+
/**
14+
* Type of meta options to be passed when defining a new safe action.
15+
*/
16+
export type ActionMetadata = {
17+
actionName?: string;
18+
};
19+
20+
/**
21+
* Type of the result of a middleware function. It extends the result of a safe action with
22+
* `parsedInput` and `ctx` optional properties.
23+
*/
24+
export type MiddlewareResult<NextCtx> = SafeActionResult<any, unknown, NextCtx> & {
25+
parsedInput?: unknown;
26+
ctx?: unknown;
27+
success: boolean;
28+
};
29+
30+
/**
31+
* Type of the middleware function passed to a safe action client.
32+
*/
33+
export type MiddlewareFn<ClientInput, Ctx, NextCtx> = {
34+
(opts: {
35+
clientInput: ClientInput;
36+
ctx: Ctx;
37+
metadata: ActionMetadata;
38+
next: {
39+
<const NC>(opts: { ctx: NC }): Promise<MiddlewareResult<NC>>;
40+
};
41+
}): Promise<MiddlewareResult<NextCtx>>;
42+
};
43+
44+
/**
45+
* Type of the function that executes server code when defining a new safe action.
46+
*/
47+
export type ServerCodeFn<S extends Schema, Data, Context> = (
48+
parsedInput: Infer<S>,
49+
utils: { ctx: Context; metadata: ActionMetadata }
50+
) => Promise<Data>;
51+
52+
/**
53+
* Type of the result of a safe action.
54+
*/
55+
// eslint-disable-next-line
56+
export type SafeActionResult<S extends Schema, Data, NextCtx = unknown> = {
57+
data?: Data;
58+
serverError?: string;
59+
validationErrors?: ValidationErrors<S>;
60+
};
61+
62+
/**
63+
* Type of the function called from components with typesafe input data.
64+
*/
65+
export type SafeActionFn<S extends Schema, Data> = (
66+
input: InferIn<S>
67+
) => Promise<SafeActionResult<S, Data>>;
+1-59
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,8 @@
1-
import type { ValidationIssue } from "@typeschema/core";
2-
import type { Schema } from "@typeschema/main";
3-
import type { ValidationErrors } from ".";
1+
export const DEFAULT_SERVER_ERROR = "Something went wrong while executing the operation.";
42

53
export const isError = (error: unknown): error is Error => error instanceof Error;
64

75
// UTIL TYPES
86

97
// Returns type or promise of type.
108
export type MaybePromise<T> = Promise<T> | T;
11-
12-
// Extends an object without printing "&".
13-
export type Extend<S> = S extends infer U ? { [K in keyof U]: U[K] } : never;
14-
15-
// VALIDATION ERRORS
16-
17-
// Object with an optional list of validation errors.
18-
export type ErrorList = { _errors?: string[] } & {};
19-
20-
// Creates nested schema validation errors type using recursion.
21-
export type SchemaErrors<S> = {
22-
[K in keyof S]?: S[K] extends object | null | undefined
23-
? Extend<ErrorList & SchemaErrors<S[K]>>
24-
: ErrorList;
25-
} & {};
26-
27-
// This function is used to build the validation errors object from a list of validation issues.
28-
export const buildValidationErrors = <const S extends Schema>(issues: ValidationIssue[]) => {
29-
const ve: any = {};
30-
31-
for (const issue of issues) {
32-
const { path, message } = issue;
33-
34-
// When path is undefined or empty, set root errors.
35-
if (!path || path.length === 0) {
36-
ve._errors = ve._errors ? [...ve._errors, message] : [message];
37-
continue;
38-
}
39-
40-
// Reference to errors object.
41-
let ref = ve;
42-
43-
// Set object for the path, if it doesn't exist.
44-
for (let i = 0; i < path.length - 1; i++) {
45-
const k = path[i]!;
46-
47-
if (!ref[k]) {
48-
ref[k] = {};
49-
}
50-
51-
ref = ref[k];
52-
}
53-
54-
// Key is always the last element of the path.
55-
const key = path[path.length - 1]!;
56-
57-
// Set error for the current path. If `_errors` array exists, add the message to it.
58-
ref[key] = (
59-
ref[key]?._errors
60-
? { ...structuredClone(ref[key]), _errors: [...ref[key]._errors, message] }
61-
: { ...structuredClone(ref[key]), _errors: [message] }
62-
) satisfies ErrorList;
63-
}
64-
65-
return ve as ValidationErrors<S>;
66-
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import type { ValidationIssue } from "@typeschema/core";
2+
import type { Schema } from "@typeschema/main";
3+
import type { ErrorList, ValidationErrors } from "./validation-errors.types";
4+
5+
// This function is used internally to build the validation errors object from a list of validation issues.
6+
export const buildValidationErrors = <const S extends Schema>(issues: ValidationIssue[]) => {
7+
const ve: any = {};
8+
9+
for (const issue of issues) {
10+
const { path, message } = issue;
11+
12+
// When path is undefined or empty, set root errors.
13+
if (!path || path.length === 0) {
14+
ve._errors = ve._errors ? [...ve._errors, message] : [message];
15+
continue;
16+
}
17+
18+
// Reference to errors object.
19+
let ref = ve;
20+
21+
// Set object for the path, if it doesn't exist.
22+
for (let i = 0; i < path.length - 1; i++) {
23+
const k = path[i]!;
24+
25+
if (!ref[k]) {
26+
ref[k] = {};
27+
}
28+
29+
ref = ref[k];
30+
}
31+
32+
// Key is always the last element of the path.
33+
const key = path[path.length - 1]!;
34+
35+
// Set error for the current path. If `_errors` array exists, add the message to it.
36+
ref[key] = (
37+
ref[key]?._errors
38+
? {
39+
...structuredClone(ref[key]),
40+
_errors: [...ref[key]._errors, message],
41+
}
42+
: { ...structuredClone(ref[key]), _errors: [message] }
43+
) satisfies ErrorList;
44+
}
45+
46+
return ve as ValidationErrors<S>;
47+
};
48+
49+
// This class is internally used to throw validation errors in action's server code function, using
50+
// `returnValidationErrors`.
51+
export class ServerValidationError<S extends Schema> extends Error {
52+
public validationErrors: ValidationErrors<S>;
53+
constructor(validationErrors: ValidationErrors<S>) {
54+
super("Server Validation Error");
55+
this.validationErrors = validationErrors;
56+
}
57+
}
58+
59+
/**
60+
* Return custom validation errors to the client from the action's server code function.
61+
* Code declared after this function invocation will not be executed.
62+
* @param schema Input schema
63+
* @param validationErrors Validation errors object
64+
* @throws {ServerValidationError}
65+
*/
66+
export function returnValidationErrors<S extends Schema>(
67+
schema: S,
68+
validationErrors: ValidationErrors<S>
69+
): never {
70+
throw new ServerValidationError<S>(validationErrors);
71+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { Infer, Schema } from "@typeschema/main";
2+
3+
// Extends an object without printing "&".
4+
type Extend<S> = S extends infer U ? { [K in keyof U]: U[K] } : never;
5+
6+
// Object with an optional list of validation errors.
7+
export type ErrorList = { _errors?: string[] } & {};
8+
9+
// Creates nested schema validation errors type using recursion.
10+
type SchemaErrors<S> = {
11+
[K in keyof S]?: S[K] extends object | null | undefined
12+
? Extend<ErrorList & SchemaErrors<S[K]>>
13+
: ErrorList;
14+
} & {};
15+
16+
/**
17+
* Type of the returned object when input validation fails.
18+
*/
19+
export type ValidationErrors<S extends Schema> = Extend<ErrorList & SchemaErrors<Infer<S>>>;

‎website/docs/contributing.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ Have you found bugs or just want to ask a question? Please open a [GitHub issue]
1111

1212
### Donations
1313

14-
If you find this project useful, please consider making a [donation](https://www.paypal.com/donate/?hosted_button_id=ES9JRPSC66XKW). This is absolutely not required, but is very much appreciated, since it will help to cover the time and resources required to maintain this project. Thank you!
14+
If you find this project useful, please consider making a [donation](https://github.com/sponsors/TheEdoRan). This is absolutely not required, but is very much appreciated, since it will help to cover the time and resources required to maintain this project. Thank you!
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
---
2+
sidebar_position: 3
3+
description: Learn how to use both a basic and an authorization client at the same time in your project.
4+
---
5+
6+
7+
# Extending a base client
8+
9+
A common and recommended pattern with this library is to extend the base safe action client, to cover different use cases that you might want and/or need in your applicaton.
10+
11+
The most simple case that comes to mind is to define a base client for unauthenticated actions, and then extend it to create a client for authenticated actions, thanks to an authorization middleware:
12+
13+
```typescript title="src/lib/safe-action.ts"
14+
import { createSafeActionClient } from "next-safe-action";
15+
import { cookies } from "next/headers";
16+
import { getUserIdFromSessionId } from "./db";
17+
18+
// This is our base client.
19+
// Here we define a middleware that logs the result of the action execution.
20+
export const actionClient = createSafeActionClient().use(async ({ next }) => {
21+
const result = await next({ ctx: null });
22+
console.log("LOGGING MIDDLEWARE: result ->", result);
23+
return result;
24+
});
25+
26+
// This client extends the base one and ensures that the user is authenticated before running
27+
// action server code function. Note that by extending the base client, you don't need to
28+
// redeclare the logging middleware, is will simply be inherited by the new client.
29+
export const authActionClient = actionClient
30+
// Clone the base client to extend this one with additional middleware functions.
31+
.clone()
32+
// In this case, context is used for (fake) auth purposes.
33+
.use(async ({ next }) => {
34+
const session = cookies().get("session")?.value;
35+
36+
// If the session is not found, we throw an error and stop execution here.
37+
if (!session) {
38+
throw new Error("Session not found!");
39+
}
40+
41+
// In the real world, you would check if the session is valid by querying a database.
42+
// We'll keep it very simple here.
43+
const userId = await getUserIdFromSessionId(session);
44+
45+
// If the session is not valid, we throw an error and stop execution here.
46+
if (!userId) {
47+
throw new Error("Session is not valid!");
48+
}
49+
50+
// Here we return the context object for the next middleware in the chain/server code function.
51+
return next({
52+
ctx: {
53+
userId,
54+
},
55+
});
56+
});
57+
```

‎website/docs/getting-started.md

+19-15
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ For Next.js >= 14, assuming you want to use Zod as your validation library, use
2929
npm i next-safe-action zod @typeschema/zod
3030
```
3131

32+
Find the adapter for your validation library of choice in the [TypeSchema documentation](https://typeschema.com/#coverage).
33+
3234
## Usage
3335

3436
### 1. Instantiate a new client
@@ -38,44 +40,46 @@ npm i next-safe-action zod @typeschema/zod
3840
```typescript title="src/lib/safe-action.ts"
3941
import { createSafeActionClient } from "next-safe-action";
4042

41-
export const action = createSafeActionClient();
43+
export const actionClient = createSafeActionClient();
4244
```
4345

44-
This is a basic client, without any options. If you want to explore the full set of options, check out the [safe action client](/docs/safe-action-client) section.
46+
This is a basic client, without any options or middleware functions. If you want to explore the full set of options, check out the [safe action client](/docs/safe-action-client) section.
4547

4648
### 2. Define a new action
4749

48-
This is how a safe action is created. Providing a validation input schema to the function, we're sure that data that comes in is type safe and validated.
49-
The second argument of this function is an async function that receives the parsed input, and defines what happens on the server when the action is called from client. In short, this is your server code. It never runs on the client:
50+
This is how a safe action is created. Providing a validation input schema to the function via `schema()`, we're sure that data that comes in is type safe and validated.
51+
The `define()` method lets you define what happens on the server when the action is called from client, via an async function that receives the parsed input and context as arguments. In short, this is your _server code_. **It never runs on the client**:
5052

5153
```typescript title="src/app/login-action.ts"
5254
"use server"; // don't forget to add this!
5355

5456
import { z } from "zod";
55-
import { action } from "@/lib/safe-action";
57+
import { actionClient } from "@/lib/safe-action";
5658

5759
// This schema is used to validate input from client.
5860
const schema = z.object({
5961
username: z.string().min(3).max(10),
6062
password: z.string().min(8).max(100),
6163
});
6264

63-
export const loginUser = action(schema, async ({ username, password }) => {
64-
if (username === "johndoe" && password === "123456") {
65-
return {
66-
success: "Successfully logged in",
67-
};
68-
}
69-
70-
return { failure: "Incorrect credentials" };
71-
});
65+
export const loginUser = actionClient
66+
.schema(schema)
67+
.define(async ({ username, password }) => {
68+
if (username === "johndoe" && password === "123456") {
69+
return {
70+
success: "Successfully logged in",
71+
};
72+
}
73+
74+
return { failure: "Incorrect credentials" };
75+
});
7276
```
7377

7478
`action` returns a new function that can be called from the client.
7579

7680
### 3. Import and execute the action
7781

78-
In this example, we're **directly** calling the Server Actions from a Client Component. The action is passed as a prop to the component, and we can infer its type by simply using `typeof`:
82+
In this example, we're **directly** calling the Server Action from a Client Component:
7983

8084
```tsx title="src/app/login.tsx"
8185
"use client"; // this is a Client Component

‎website/docs/introduction.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
---
22
sidebar_position: 1
3-
description: next-safe-action is a library that takes full advantage of the latest and greatest Next.js, React and TypeScript features, using validation libraries of your choice, to let you define typesafe Server Actions and execute them inside Client Components.
3+
description: next-safe-action is a library that takes full advantage of the latest and greatest Next.js, React and TypeScript features, using validation libraries of your choice, to let you define type safe Server Actions and execute them inside React Components.
44
---
55

66
# Introduction
77

8-
**next-safe-action** is a library that takes full advantage of the latest and greatest Next.js, React and TypeScript features, using validation libraries of your choice, to let you define **typesafe** Server Actions and execute them inside Client Components.
8+
**next-safe-action** is a library that takes full advantage of the latest and greatest Next.js, React and TypeScript features, using validation libraries of your choice, to let you define **type safe** Server Actions and execute React Components.
99

1010
## How does it work?
1111

@@ -19,7 +19,7 @@ Your browser does not support the video tag.
1919
## Features
2020
- ✅ Pretty simple
2121
- ✅ End-to-end type safety
22-
-Context based clients (with middlewares)
22+
-Powerful middleware system
2323
- ✅ Input validation using multiple validation libraries
2424
- ✅ Advanced server error handling
2525
- ✅ Optimistic updates

‎website/docs/safe-action-client/defining-multiple-clients.md

-41
This file was deleted.

‎website/docs/safe-action-client/index.md

+3-4
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,13 @@ description: Safe action client is the instance that you can use to create types
55

66
# Safe action client
77

8-
The safe action client instance is created by the `createSafeActionClient()` function. The instance is used to create safe actions, as you have already seen in previous sections of the documentation. You can create multiple clients too, for different purposes, as explained [in this section](/docs/safe-action-client/defining-multiple-clients).
8+
The safe action client instance is created by the `createSafeActionClient()` function. The instance is used to create safe actions, as you have already seen in previous sections of the documentation. You can create multiple clients too, for different purposes, as explained [in this section](/docs/examples/extending-base-client).
99

1010
You can also provide functions to the client, to customize the behavior for every action you then create with it. We will explore them in detail in the following sections.
1111

1212
Here's a reference of all the available optional functions:
1313

1414
| Function name | Purpose |
1515
|------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
16-
| `middleware?` | Performs custom logic before action server code is executed, but after input from the client is validated. More information [here](/docs/safe-action-client/using-a-middleware). |
17-
| `handleReturnedServerError?` | When an error occurs on the server after executing the action on the client, it lets you define custom logic to returns a custom `serverError` message instead of the default one. More information [here](/docs/safe-action-client/custom-server-error-handling#handlereturnedservererror). |
18-
| `handleServerErrorLog?` | When an error occurs on the server after executing the action on the client, it lets you define custom logic to log the error on the server. By default the error is logged via `console.error`. More information [here](/docs/safe-action-client/custom-server-error-handling#handleservererrorlog). |
16+
| `handleReturnedServerError?` | When an error occurs on the server after executing the action on the client, it lets you define custom logic to returns a custom `serverError` message instead of the default one. More information [here](/docs/safe-action-client/initialization-options#handlereturnedservererror). |
17+
| `handleServerErrorLog?` | When an error occurs on the server after executing the action on the client, it lets you define custom logic to log the error on the server. By default the error is logged via `console.error`. More information [here](/docs/safe-action-client/initialization-options#handleservererrorlog). |

‎website/docs/safe-action-client/custom-server-error-handling.md ‎website/docs/safe-action-client/initialization-options.md

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
---
2-
sidebar_position: 2
3-
description: Learn how to customize server error handling.
2+
sidebar_position: 1
3+
description: You can initialize a safe action client with these options.
44
---
55

6-
# Custom server error handling
6+
# Initialization options
77

8-
### `handleReturnedServerError?`
8+
## `handleReturnedServerError?`
99

1010
You can provide this optional function to the safe action client. It is used to customize the server error message returned to the client, if one occurs during action's server execution. This includes errors thrown by the action server code, and errors thrown by the middleware.
1111

1212
Here's a simple example, changing the message for every error thrown on the server:
1313

1414
```typescript title=src/lib/safe-action.ts
15-
export const action = createSafeActionClient({
15+
export const actionClient = createSafeActionClient({
1616
// Can also be an async function.
1717
handleReturnedServerError(e) {
1818
return "Oh no, something went wrong!";
@@ -29,7 +29,7 @@ import { DEFAULT_SERVER_ERROR } from "next-safe-action";
2929

3030
class MyCustomError extends Error {}
3131

32-
export const action = createSafeActionClient({
32+
export const actionClient = createSafeActionClient({
3333
// Can also be an async function.
3434
handleReturnedServerError(e) {
3535
// In this case, we can use the 'MyCustomError` class to unmask errors
@@ -44,14 +44,14 @@ export const action = createSafeActionClient({
4444
});
4545
```
4646

47-
### `handleServerErrorLog?`
47+
## `handleServerErrorLog?`
4848

4949
You can provide this optional function to the safe action client. This is used to define how errors should be logged when one occurs while the server is executing an action. This includes errors thrown by the action server code, and errors thrown by the middleware. Here you get as argument the **original error object**, not a message customized by `handleReturnedServerError`, if provided.
5050

5151
Here's a simple example, logging error to the console while also reporting it to an error handling system:
5252

5353
```typescript title=src/lib/safe-action.ts
54-
export const action = createSafeActionClient({
54+
export const actionClient = createSafeActionClient({
5555
// Can also be an async function.
5656
handleServerErrorLog(e) {
5757
// We can, for example, also send the error to a dedicated logging system.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
sidebar_position: 2
3+
description: List of methods of the safe action client.
4+
---
5+
6+
# Instance methods
7+
8+
`createSafeActionClient` creates an instance of the safe action client, which has the following methods:
9+
10+
## `clone`
11+
12+
```typescript
13+
actionClient.clone() => new SafeActionClient()
14+
```
15+
16+
`clone` returns a new instance of the safe action client with the same initialization options and middleware functions as the original one. It is used to extend a base client with additional middleware functions. If you don't use `clone` when creating a new client, the middleware function list of the original one will be mutated and extended with the new ones, which is not desirable.
17+
18+
## `use`
19+
20+
```typescript
21+
use<const ClientInput, const NextCtx>(middlewareFn: MiddlewareFn<ClientInput, Ctx, NextCtx>) => new SafeActionClient()
22+
```
23+
24+
`use` accepts a middleware function of type [`MiddlewareFn`](/docs/types#middlewarefn) as argument and returns a new instance of the safe action client with that middleware function added to the stack, that will be executed after the last one, if any. Check out how to `use` middleware in [the related section](/docs/usage/middleware) of the usage guide.
25+
26+
## `metadata`
27+
28+
```typescript
29+
metadata(data: ActionMetadata) => { schema() }
30+
```
31+
32+
`metadata` expects an object of type [`ActionMetadata`](/docs/types#actionmetadata) that lets you specify useful data about the safe action you're defining, and it returns the [`schema`](#schema) method, since metadata is action specific and not shared with other actions. As of now, the only data you can pass in is the `actionName`, but that could be extended in the future. You can then access it in the `middlewareFn` passed to [`use`](#use) and in [`serverCodeFn`](#servercodefn) passed to [`define`](#define).
33+
34+
## `schema`
35+
36+
```typescript
37+
schema<const S extends Schema>(schema: S) => { define() }
38+
```
39+
40+
`schema` accepts an input schema of type `Schema` (from TypeSchema), which is used to define the arguments that the safe action will receive, and returns the [`define`](#define) method, which allows you to define a new action using that input schema.
41+
42+
## `define`
43+
44+
```typescript
45+
define<const Data = null>(serverCodeFn: ServerCodeFn<S, Data, Ctx>) => SafeActionFn<S, Data>
46+
```
47+
48+
`define` is the final method in the list. It accepts a [`serverCodeFn`](#servercodefn) of type [`ServerCodeFn`](/docs/types#servercodefn) and returns a new safe action function of type [`SafeActionFn`](/docs/types#safeactionfn), which can be called from your components.
49+
50+
When the action is executed, all middleware functions in the chain will be called at runtime, in the order they were defined.
51+
52+
### `serverCodeFn`
53+
54+
```typescript
55+
serverCodeFn<S extends Schema, Data, Context> = (parsedInput: Infer<S>, utils: { ctx: Context; metadata: ActionMetadata }) => Promise<Data>;
56+
```
57+
58+
`serverCodeFn` is the async function that will be executed on the **server side** when the action is invoked. If input validation fails, or execution gets halted in a middleware function, the server code function will not be called.

‎website/docs/safe-action-client/using-a-middleware.md

-109
This file was deleted.

‎website/docs/types.md

+62-14
Original file line numberDiff line numberDiff line change
@@ -7,38 +7,64 @@ description: List of next-safe-action types.
77

88
## /
99

10-
### `SafeClientOpts`
10+
### `SafeActionClientOpts`
1111

1212
Type of options when creating a new safe action client.
1313

1414
```typescript
15-
export type SafeClientOpts<Context, MiddlewareData> = {
15+
export type SafeActionClientOpts = {
1616
handleServerErrorLog?: (e: Error) => MaybePromise<void>;
1717
handleReturnedServerError?: (e: Error) => MaybePromise<string>;
18-
middleware?: (parsedInput: any, data?: MiddlewareData) => MaybePromise<Context>;
1918
};
2019
```
2120

22-
### `SafeAction`
21+
### `ActionMetadata`
2322

24-
Type of the function called from Client Components with typesafe input data.
23+
Type of meta options to be passed when defining a new safe action.
2524

2625
```typescript
27-
type SafeAction<S extends Schema, Data> = (input: InferIn<S>) => Promise<{
28-
data?: Data;
29-
serverError?: string;
30-
validationErrors?: Partial<Record<keyof Infer<S> | "_root", string[]>>;
31-
}>;
26+
export type ActionMetadata = {
27+
actionName?: string;
28+
};
29+
```
30+
31+
### `MiddlewareResult`
32+
33+
Type of the result of a middleware function. It extends the result of a safe action with `parsedInput` and `ctx` optional properties.
34+
35+
```typescript
36+
export type MiddlewareResult<NextCtx> = SafeActionResult<any, unknown, NextCtx> & {
37+
parsedInput?: unknown;
38+
ctx?: unknown;
39+
success: boolean;
40+
};
41+
```
42+
43+
### `MiddlewareFn`
44+
45+
Type of the middleware function passed to a safe action client.
46+
47+
```typescript
48+
export type MiddlewareFn<ClientInput, Ctx, NextCtx> = {
49+
(opts: {
50+
clientInput: ClientInput;
51+
ctx: Ctx;
52+
metadata: ActionMetadata;
53+
next: {
54+
<const NC>(opts: { ctx: NC }): Promise<MiddlewareResult<NC>>;
55+
};
56+
}): Promise<MiddlewareResult<NextCtx>>;
57+
};
3258
```
3359

3460
### `ServerCodeFn`
3561

3662
Type of the function that executes server code when defining a new safe action.
3763

3864
```typescript
39-
type ServerCodeFn<S extends Schema, Data, Context> = (
65+
export type ServerCodeFn<S extends Schema, Data, Context> = (
4066
parsedInput: Infer<S>,
41-
ctx: Context
67+
utils: { ctx: Context; metadata: ActionMetadata }
4268
) => Promise<Data>;
4369
```
4470

@@ -50,6 +76,28 @@ Type of the returned object when input validation fails.
5076
export type ValidationErrors<S extends Schema> = Extend<ErrorList & SchemaErrors<Infer<S>>>;
5177
```
5278

79+
### `SafeActionResult`
80+
81+
Type of the result of a safe action.
82+
83+
```typescript
84+
export type SafeActionResult<S extends Schema, Data, NextCtx = unknown> = {
85+
data?: Data;
86+
serverError?: string;
87+
validationErrors?: ValidationErrors<S>;
88+
};
89+
```
90+
91+
### `SafeActionFn`
92+
93+
Type of the function called from components with typesafe input data.
94+
95+
```typescript
96+
export type SafeActionFn<S extends Schema, Data> = (
97+
input: InferIn<S>
98+
) => Promise<SafeActionResult<S, Data>>;
99+
```
100+
53101
## /hooks
54102

55103
### `HookResult`
@@ -59,7 +107,7 @@ Type of `result` object returned by `useAction` and `useOptimisticAction` hooks.
59107
If a server-client communication error occurs, `fetchError` will be set to the error message.
60108

61109
```typescript
62-
type HookResult<S extends Schema, Data> = Awaited<ReturnType<SafeAction<S, Data>>> & {
110+
type HookResult<S extends Schema, Data> = SafeActionResult<S, Data> & {
63111
fetchError?: string;
64112
};
65113
```
@@ -137,4 +185,4 @@ export type SchemaErrors<S> = {
137185

138186
## TypeSchema library
139187

140-
`Infer`, `InferIn`, `Schema` types come from [TypeSchema](https://typeschema.com/#types) library.
188+
`Infer`, `InferIn`, `Schema` types come from [TypeSchema](https://typeschema.com) library.

‎website/docs/usage/action-result-object.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
sidebar_position: 3
2+
sidebar_position: 4
33
description: Action result object is the result of an action execution.
44
---
55

@@ -12,4 +12,4 @@ Here's how action result object is structured (all keys are optional):
1212
|--------------------|--------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
1313
| `data?` | Execution is successful. | What you returned in action's server code. |
1414
| `validationErrors?` | Input data doesn't pass validation. | An object whose keys are the names of the fields that failed validation. Each key's value is either an `ErrorList` or a nested key with an `ErrorList` inside.<br />`ErrorList` is defined as: `{ errors?: string[] }`.<br />It follows the same structure as [Zod's `format` function](https://zod.dev/ERROR_HANDLING?id=formatting-errors).
15-
| `serverError?` | An error occurs during action's server code execution. | A `string` that by default is "Something went wrong while executing the operation" for every server error that occurs, but this is [configurable](/docs/safe-action-client/custom-server-error-handling#handlereturnedservererror) when instantiating a new client. |
15+
| `serverError?` | An error occurs during action's server code execution. | A `string` that by default is "Something went wrong while executing the operation" for every server error that occurs, but this is [configurable](/docs/safe-action-client/initialization-options#handlereturnedservererror) when instantiating a new client. |

‎website/docs/usage/client-components/direct-execution.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description: You can execute safe actions by directrly calling them inside Clien
55

66
# 1. Direct execution
77

8-
The first way to execute Server Actions inside Client Components is by passing the action from a Server Component to a Client Component and directly calling it in a function. This method is the most simple one, but in some cases it could be all you need, for example if you just need the action result inside an `onClick` or `onSubmit` handler, without overcomplicating things:
8+
The first way to execute Server Actions inside Client Components is by importing it and directly calling it in a function. This method is the simplest one, but in some cases it could be all you need, for example if you just need the action result inside an `onClick` or `onSubmit` handlers, without overcomplicating things:
99

1010
```tsx
1111
export default function Login({ loginUser }: Props) {
@@ -27,4 +27,4 @@ export default function Login({ loginUser }: Props) {
2727

2828
Every action you execute returns an object with the same structure. This is described in the [action result object](/docs/usage/action-result-object) section.
2929

30-
Explore a working example [here](https://github.com/TheEdoRan/next-safe-action/tree/main/packages/example-app/src/app).
30+
Explore a working example [here](https://github.com/TheEdoRan/next-safe-action/tree/main/packages/example-app/src/app/(examples)/direct).

‎website/docs/usage/client-components/hooks/useaction.md

+7-5
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ const schema = z.object({
2121
name: z.string(),
2222
});
2323

24-
export const greetUser = action(schema, async ({ name }) => {
25-
return { message: `Hello ${name}!` };
26-
});
24+
export const greetUser = actionClient
25+
.schema(schema)
26+
.define(async ({ name }) => {
27+
return { message: `Hello ${name}!` };
28+
});
2729
```
2830

2931
2. In your Client Component, you can use it like this:
@@ -59,7 +61,7 @@ As you can see, here we display a greet message after the action is performed, i
5961

6062
| Name | Type | Purpose |
6163
|--------------|--------------------------------------------|--------------------------------------------------------------------------------------------------|
62-
| `safeAction` | [SafeAction](/docs/types#safeaction) | This is the action that will be called when you use `execute` from hook's return object. |
64+
| `safeActionFn` | [SafeActionFn](/docs/types#safeactionfn) | This is the action that will be called when you use `execute` from hook's return object. |
6365
| `callbacks?` | [HookCallbacks](/docs/types#hookcallbacks) | Optional callbacks. More information about them [here](/docs/usage/client-components/hooks/callbacks). |
6466

6567
### `useAction` return object
@@ -73,4 +75,4 @@ As you can see, here we display a greet message after the action is performed, i
7375
| `status` | [`HookActionStatus`](/docs/types#hookresult) | The action current status. |
7476
| `reset` | `() => void` | You can programmatically reset the `result` object with this function. |
7577

76-
Explore a working example [here](https://github.com/TheEdoRan/next-safe-action/tree/main/packages/example-app/src/app/hook).
78+
Explore a working example [here](https://github.com/TheEdoRan/next-safe-action/tree/main/packages/example-app/src/app/(examples)/hook).

‎website/docs/usage/client-components/hooks/useoptimisticaction.md

+15-13
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,20 @@ const schema = z.object({
2929
let likes = 42;
3030
export const getLikes = () => likes;
3131

32-
export const addLikes = action(schema, async ({ amount }) => {
33-
await new Promise((resolve) => setTimeout(resolve, 1000));
32+
export const addLikes = actionClient
33+
.schema(schema)
34+
.define(async ({ amount }) => {
35+
await new Promise((resolve) => setTimeout(resolve, 1000));
3436

35-
// Mutate data in fake db. This would be a database call in the real world.
36-
likes += amount;
37+
// Mutate data in fake db. This would be a database call in the real world.
38+
likes += amount;
3739

38-
// We use this function to revalidate server state.
39-
// More information about it here:
40-
// https://nextjs.org/docs/app/api-reference/functions/revalidatePath
41-
revalidatePath("/");
40+
// We use this function to revalidate server state.
41+
// More information about it here:
42+
// https://nextjs.org/docs/app/api-reference/functions/revalidatePath
43+
revalidatePath("/");
4244

43-
return { numOfLikes: likes };
45+
return { numOfLikes: likes };
4446
});
4547
```
4648

@@ -102,8 +104,8 @@ export default function AddLikes({ numOfLikes }: Props) {
102104

103105
| Name | Type | Purpose |
104106
|------------------|-----------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
105-
| `safeAction` | [`SafeAction`](/docs/types#safeaction) | This is the action that will be called when you use `execute` from hook's return object. |
106-
| `initialOptimisticData` | `Data` (return type of the `safeAction` you passed as first argument) | An initializer for the optimistic state. Usually this value comes from the parent Server Component. |
107+
| `safeActionFn` | [`SafeActionFn`](/docs/types#safeactionfn) | This is the action that will be called when you use `execute` from hook's return object. |
108+
| `initialOptimisticData` | `Data` (return type of the `safeActionFn` you passed as first argument) | An initializer for the optimistic state. Usually this value comes from the parent Server Component. |
107109
| `reducer` | `(state: Data, input: InferIn<S>) => Data` | When you call the action via `execute`, this function determines how the optimistic update is performed. Basically, here you define what happens **immediately** after `execute` is called, and before the actual result comes back from the server. |
108110
| `callbacks?` | [`HookCallbacks`](/docs/types#hookcallbacks) | Optional callbacks. More information about them [here](/docs/usage/client-components/hooks/callbacks). |
109111

@@ -118,6 +120,6 @@ export default function AddLikes({ numOfLikes }: Props) {
118120
| `result` | [`HookResult`](/docs/types#hookresult) | When the action gets called via `execute`, this is the result object. |
119121
| `status` | [`HookActionStatus`](/docs/types#hookresult) | The action current status. |
120122
| `reset` | `() => void` | You can programmatically reset the `result` object with this function. |
121-
| `optimisticData` | `Data` (return type of the `safeAction` you passed as first argument) | This is the data that gets updated immediately after `execute` is called, with the behavior you defined in the `reducer` function hook argument. The initial state is what you provided to the hook via `initialOptimisticData` argument. |
123+
| `optimisticData` | `Data` (return type of the `safeActionFn` you passed as first argument) | This is the data that gets updated immediately after `execute` is called, with the behavior you defined in the `reducer` function hook argument. The initial state is what you provided to the hook via `initialOptimisticData` argument. |
122124

123-
Explore a working example [here](https://github.com/TheEdoRan/next-safe-action/tree/main/packages/example-app/src/app/optimistic-hook).
125+
Explore a working example [here](https://github.com/TheEdoRan/next-safe-action/tree/main/packages/example-app/src/app/(examples)/optimistic-hook).

‎website/docs/usage/client-components/index.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ sidebar_label: Client Components
66

77
# Usage with Client Components
88

9-
There are three different ways to execute Server Actions from Client Components. First one is the "direct way", the most simple one, but the least powerful too. The other two are by using `useAction` and `useOptimisticAction` hooks, which we will cover in the next sections.
9+
There are three different ways to execute Server Actions from Client Components. First one is the "direct way", the simplest one, but the least powerful too. The other two are by using `useAction` and `useOptimisticAction` hooks, which we will cover in the next sections.

‎website/docs/usage/custom-validation-errors.md

+15-13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
sidebar_position: 4
2+
sidebar_position: 5
33
description: Set custom validation errors in schema or in action's server code function.
44
---
55

@@ -39,18 +39,20 @@ import { returnValidationErrors } from "next-safe-action";
3939
import { action } from "@/lib/safe-action";
4040

4141
// Here we're using the same schema declared above.
42-
const signupAction = action(schema, async ({email}) => {
43-
// Assume this is a database call.
44-
if (!isEmailAvailable(email)) {
45-
returnValidationErrors(schema, {
46-
email: {
47-
_errors: ["Email already registered"],
48-
},
49-
});
50-
}
51-
52-
...
53-
});
42+
const signupAction = actionClient
43+
.schema(schema)
44+
.define(async ({ email }) => {
45+
// Assume this is a database call.
46+
if (!isEmailAvailable(email)) {
47+
returnValidationErrors(schema, {
48+
email: {
49+
_errors: ["Email already registered"],
50+
},
51+
});
52+
}
53+
54+
...
55+
});
5456
```
5557

5658
Note that:

‎website/docs/usage/forms.md

+7-6
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,15 @@ const schema = zfd.formData({
2626
password: zfd.text(z.string().min(8)),
2727
});
2828

29-
export const signup = action(schema, async ({ email, password }) => {
30-
console.log("Email:", email, "Password:", password);
31-
32-
// Do something useful here.
33-
});
29+
export const signup = action
30+
.schema(schema)
31+
.define(async ({ email, password }) => {
32+
console.log("Email:", email, "Password:", password);
33+
// Do something useful here.
34+
});
3435
```
3536

36-
2. Import it in a Server Component and use it as a form action.
37+
2. Import it in a Server Component and use it as a Form Action.
3738

3839
```tsx title=src/app/signup/page.tsx
3940
import { signup } from "./signup-action";

‎website/docs/usage/middleware.md

+195
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
---
2+
sidebar_position: 3
3+
description: Learn how to use middleware functions in your actions.
4+
---
5+
6+
# Middleware
7+
8+
next-safe-action, since version 7, ships with a composable and powerful middleware system, which allows you to create functions for almost every kind of use case you can imagine (authorization, logging, role based access, etc.). It works very similarly to the [tRPC implementation](https://trpc.io/docs/server/middlewares), with some minor differences.
9+
10+
Middleware functions are defined using [`use`](/docs/safe-action-client/instance-methods#use) method in your action clients, via the `middlewareFn` argument.
11+
12+
## Usage
13+
14+
You can chain multiple middleware functions, that will be executed in the order they were defined. You can also await the next middleware function(s) in the stack (useful, for instance, for logging), and then return the result of the execution. Chaining functions is very useful when you want to dynamically extend the context and/or halt execution based on your use case.
15+
16+
### Instance level middleware
17+
18+
Instance level is the right place when you want to share middleware behavior for all the actions you're going to define; for example when you need to log the result of actions execution, or verify if the user intending to execute the operation is authorized to do so, and if not, halt the execution at that point, by throwing an error.
19+
20+
Here we'll use a logging middleware in the base client and then extend it with an authorization middleware in `authActionClient`. We'll also define a safe action called `editProfile`, that will use `authActionClient` as its client. Note that the `handleReturnedServerError` function passed to the base client will also be used for `authActionClient`:
21+
22+
```typescript title="src/lib/safe-action.ts"
23+
import { createSafeActionClient } from "next-safe-action";
24+
import { cookies } from "next/headers";
25+
import { getUserIdFromSessionId } from "./db";
26+
27+
class ActionError extends Error {}
28+
29+
// Base client.
30+
const actionClient = createSafeActionClient({
31+
handleReturnedServerError(e) {
32+
if (e instanceof ActionError) {
33+
return e.message;
34+
}
35+
36+
return DEFAULT_SERVER_ERROR;
37+
},
38+
// Define logging middleware.
39+
}).use(async ({ next, clientInput, metadata }) => {
40+
console.log("LOGGING MIDDLEWARE");
41+
42+
// Here we await the action execution.
43+
const result = await next({ ctx: null });
44+
45+
console.log("Result ->", result);
46+
console.log("Client input ->", clientInput);
47+
console.log("Metadata ->", metadata);
48+
49+
// And then return the result of the awaited action.
50+
return result;
51+
});
52+
53+
// Auth client defined by extending the base one.
54+
// Note that the same initialization options and middleware functions of the base client
55+
// will also be used for this one.
56+
const authActionClient = actionClient
57+
// Clone the base client so it doesn't get mutated.
58+
.clone()
59+
// Define authorization middleware.
60+
.use(async ({ next }) => {
61+
const session = cookies().get("session")?.value;
62+
63+
if (!session) {
64+
throw new Error("Session not found!");
65+
}
66+
67+
const userId = await getUserIdFromSessionId(session);
68+
69+
if (!userId) {
70+
throw new Error("Session is not valid!");
71+
}
72+
73+
// Return the next middleware with `userId` value in the context
74+
return next({ ctx: { userId } });
75+
});
76+
```
77+
78+
Here we import `authActionClient` in the action's file:
79+
80+
```typescript title="src/app/edituser-action.ts"
81+
"use server";
82+
83+
import { authActionClient } from "@/lib/safe-action";
84+
import { z } from "zod";
85+
86+
const editProfile = authActionClient
87+
// We can pass the action name inside `metadata()`.
88+
.metadata({ actionName: "editProfile" })
89+
// Here we pass the input schema.
90+
.schema(z.object({ newUsername: z.string() }))
91+
// Here we get `userId` from the middleware defined in `authActionClient`.
92+
.define(async ({ newUsername }, { ctx: { userId } }) => {
93+
await saveNewUsernameInDb(userId, newUsername);
94+
95+
return {
96+
updated: true,
97+
};
98+
});
99+
```
100+
101+
Calling `editProfile` will produce this console output, thanks to the two middleware functions passed to the clients above:
102+
103+
```
104+
LOGGING MIDDLEWARE
105+
Result -> {
106+
success: true,
107+
ctx: { userId: 'e473de7f-d1e4-49c1-b4fe-85eb50048b99' },
108+
data: { updated: true },
109+
parsedInput: { newUsername: 'johndoe' }
110+
}
111+
Client input -> { newUsername: 'johndoe' }
112+
Metadata -> { actionName: 'editProfile' }
113+
```
114+
115+
Note that `userId` in `ctx` comes from the `authActionClient` middleware, and console output comes from the logging middleware defined in the based client.
116+
117+
### Action level middleware
118+
119+
Server Action level is the right place for middleware checks that only specific actions need to make. For instance, when you want to restrict the execution to specific user roles.
120+
121+
In this example we'll use the same `authActionClient` defined above to define a `deleteUser` action that chains a middleware function which restricts the execution of this operation to just admins:
122+
123+
```typescript title="src/app/deleteuser-action.ts"
124+
"use server";
125+
126+
import { authActionClient } from "@/lib/safe-action";
127+
import { z } from "zod";
128+
129+
const deleteUser = authActionClient
130+
.use(async ({ next, ctx }) => {
131+
// `userId` comes from the context set in the previous middleware function.
132+
const userRole = await getUserRole(ctx.userId);
133+
134+
if (userRole !== "admin") {
135+
throw new ActionError("Only admins can delete users.");
136+
}
137+
138+
// Here we pass the same untouched context (`userId`) to the next function, since we don't need
139+
// to add data to the context here.
140+
return next({ ctx });
141+
})
142+
.metadata({ actionName: "deleteUser" })
143+
.schema(z.void())
144+
.define(async (_, { ctx: { userId } }) => {
145+
// Here we're sure that the user that is performing this operation is an admin.
146+
await deleteUserFromDb(userId);
147+
});
148+
```
149+
150+
This is the console output when an admin executes this action:
151+
152+
```
153+
LOGGING MIDDLEWARE
154+
Result -> {
155+
success: true,
156+
ctx: { userId: '9af18417-524e-4f04-9621-b5934b09f2c9' },
157+
data: null,
158+
parsedInput: undefined
159+
}
160+
Client input -> undefined
161+
Metadata -> { actionName: 'deleteUser' }
162+
```
163+
164+
If a regular user tries to do the same, the execution will be stopped at the last middleware function, defined at the action level, that checks the user role. This is the console output in this case:
165+
166+
```
167+
LOGGING MIDDLEWARE
168+
Action error: Only admins can delete users.
169+
Result -> {
170+
success: false,
171+
ctx: { userId: '0a1fa8a8-d323-47c0-bbde-eadbfcdd2587' },
172+
serverError: 'Only admins can delete users.'
173+
}
174+
Client input -> undefined
175+
Metadata -> { actionName: 'deleteUser' }
176+
```
177+
178+
Note that the second line comes from the default `handleServerErrorLog` function of the safe action client(s).
179+
180+
---
181+
182+
## `middlewareFn` arguments
183+
184+
`middlewareFn` has the following arguments:
185+
186+
| Name | Type | Purpose |
187+
|---------------|----------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
188+
| `clientInput` | `ClientInput` (generic) | The raw input (not parsed) passed from client. |
189+
| `ctx` | `Ctx` (generic) | Type safe context value from previous middleware function(s). |
190+
| `metadata` | [`ActionMetadata`](/docs/types/#actionmetadata) | Metadata for the safe action execution. |
191+
| `next` | `<const NC>(opts: { ctx: NC }): Promise<MiddlewareResult<NC>>` | Function that will execute the next function in the middleware stack or the server code function. It expects, as argument, the next `ctx` value for the next function in the chain. |
192+
193+
## `middlewareFn` return value
194+
195+
`middlewareFn` returns a Promise of a [`MiddlewareResult`](/docs/types#middlewareresult) object. It extends the result of a safe action with `success` property, and `parsedInput` and `ctx` optional properties. This is the exact return type of the `next` function, so you must always return it (or its result) to continue executing the middleware chain.

‎website/src/components/landing/features.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ const features: { title: string; description: string }[] = [
1313
"With next-safe-action you get full type safety between server and client code.",
1414
},
1515
{
16-
title: "Context-based clients (with middlewares)",
16+
title: "Powerful middleware system",
1717
description:
18-
"Powerful context-based clients with custom logic execution, thanks to middlewares.",
18+
"Manage authorization, log and halt execution, and much more with a composable middleware system.",
1919
},
2020
{
2121
title: "Input validation using multiple validation libraries",

0 commit comments

Comments
 (0)
Please sign in to comment.