v7 - Rethinking middleware and other updates #88
Replies: 8 comments 7 replies
-
I love the amount of thought you've put into this and the desire to make this library even more useful! 💯 Here are my thoughts after reading this and trying out v7 in my own project:
export const someAction = authAction
.schema(z.void())
.define(async (_, { ctx: { someContextValue } }) => {
console.log(someContextValue)
}); But what if we could instead make export const someAction = authAction
// 👇`clientInput` is unknown if you do not call `.schema` before defining the action.
.define(async ({ clientInput, ctx: { someContextValue } }) => {
console.log(someContextValue)
}); This would have the additional benefit of aligning the API of
|
Beta Was this translation helpful? Give feedback.
-
Update: renamed P.S.: |
Beta Was this translation helpful? Give feedback.
-
Update: metadata type is now generic in the latest |
Beta Was this translation helpful? Give feedback.
-
putting function in one place causing metadata get overwritten
|
Beta Was this translation helpful? Give feedback.
-
Update: removed |
Beta Was this translation helpful? Give feedback.
-
Nice to see the simplification of not having to pass a schema to an action if you don't need any parameters 💯 I think a more easily understood API would be not having to call |
Beta Was this translation helpful? Give feedback.
-
Update:
|
Beta Was this translation helpful? Give feedback.
-
error next.js standalone build. adding
|
Beta Was this translation helpful? Give feedback.
-
With nearly 1k stars on GitHub and 10,000 weekly downloads on NPM (thank you!), I finally believe it's time to... rethink how middleware functions work in next-safe-action. My idea is to provide a user experience very similar to the tRPC implementation, which has greatly inspired these changes to the library, so thank you to all the tRPC contributors. This implementation has some differences though, so please keep reading this discussion if you want to know more.
As of version 6, you can pass a single
middleware
function tocreateSafeActionClient
, which returns acontext
that will be available to all actions defined with that client, via theserverCode
function, as the last argument. This works fine in some cases, but since it's a "monolithic" approach, it's not that powerful and composable, which isn't great.Let's take a look at the middleware example from the docs, and adapt it to the new implementation. This is how middleware works right now:
First, we're gonna refactor this code using the new middleware implementation, and then I'll discuss about the changes.
We can see that we no longer have to pass a
middleware
function tocreateSafeActionClient
. Instead, we define our middleware functions usinguse()
. The code inside middleware function is almost the same, with just two differences from the previous version:ctx
property as argument of the middleware function, which contains the context value from the previous middleware function.next
function, which has useful information about the action execution. Note that thenext
function expectsctx
as argument, that will overwrite the previous value. Context doesn't have to be an object, but if this is the case you can easily include the previous context value by spreading the previousctx
(argument from the middleware) into the newctx
insidenext
function.Middleware chaining
The new middleware system is much more powerful than the previous one. You can now await the next middleware function in the stack (useful, for instance, for logging), and chain multiple middleware functions, both at the instance level and at the safe action level. This is very useful when you want to dynamically extend the context and/or halt execution based on your use case.
Middleware at the instance level
Defining middleware at the instance level is useful, for example, when we need to log the result of an action execution, or declare an
authActionClient
(as above) that verifies if the user intending to execute the operation is authorized to do so, and if not, to halt the execution at that point, by throwing an error. Here we'll redeclare the two safe action clients, defining a logging middleware in the base client and using the authorization one (same as above) inauthActionClient
. We'll also define a safe action callededitProfile
, that will useauthActionClient
. Note that thehandleReturnedServerError
function passed to the base client will also be used forauthActionClient
.Calling
editProfile
will produce this console output, thanks to the two middleware functions:Middleware at the action level
Defining middleware at the Server Action level is useful, for instance, when we want to restrict the execution to specific user roles. In this example we'll use the same
authActionClient
defined above to define adeleteUser
action that chains a middleware function which restricts the execution of this operation to just admins:This is the console output when an admin executes this action:
If a regular user tries to do the same, the execution will be stopped at the middleware function that checks the user role, defined at the action level. This is the console output in this case:
Final thoughts
I think this implementation greatly improves the capabilities of the library, since it paves the way for much more complex use cases and code composability, but since this is a fundamental change in how next-safe-action works, I would be very grateful to hear what you, the community, think about these changes. Thank you all for using next-safe-action!
You can find these changes in the middleware-chaining branch. That branch will be merged in
next
branch when the implementation is finalized and documentation is updated. The new middleware system is expected to land in next-safe-action version 7.Beta Was this translation helpful? Give feedback.
All reactions