All right, so now that we know the basics of server actions and we've covered the most primitive way of using it, we're gonna create the following abstraction. So let me go ahead and expand my screen and here I've prepared something. We're gonna extract this server action which we have right now as you can see in this actions folder. Let me show you. So we have an individual action like create board.
So that represents this server action. We're going to create that into a folder which is going to have types which are where we are going to define what kind of stuff we expect the user to input and we're also going to define what exactly we expect to output from this server action which can either be an error or for example it can be a specific type from Prisma database. We're also going to create a file called schema.ts where we are going to keep our Zod validation and that schema is actually going to be our input type. And lastly we're gonna have the index.ts which is going to be the server action itself. And all of that is going to be combined inside of create safe action as you see right here.
So that's gonna be a wrapper which is going to combine all of them and that is going to leave us with the following thing. So we're going to create a hook called use action as you can see right here and inside we're going to pass this safe action which is going to be this wrapper which is going to combine all of those. And from this use action, we'll be able to extract the execute function itself, which is going to be used to call the server action. We're gonna be able to extract the data, the error, if it's a server error, like something went wrong in database, or if it is a field error, like, I don't know, the title is too short. And besides that, we're also gonna have some callbacks, so we don't have to rely on putting this stuff inside of useEffect to, I don't know, just render a success message or something.
So we're going to have an onSuccess callback, which is going to give us the data inside of its params here. And that's going to be a type of output, which we defined all the way here. So it's going to be completely type safe from start to finish on error is going to produce an error from the server and on complete is simply going to be something like finally right so regardless if we succeeded or not that's going to what use sorry that's what on complete is going to be So let's go ahead and let's now create this createSafeAction.ts. So I'm gonna go ahead inside of, first I'm gonna close everything and then I'm gonna go inside of my lib folder and create a new file called createSafeAction.ts And first thing I want to do is I want to import Zod like this and then I'm going to write export type field errors. So these are going to be generic errors, which we are able to get from Zod validation, which are going to be individual field errors.
So that's going to be a K in key of T, because the T is going to be an object with fields, which have our errors, and they are going to produce an array of strings where each string is going to be an individual error. And then let's write export type action state which will accept a generic T input and T output. That's going to be an object, which is going to have optional field errors, which is a type of field errors, which is going to validate the input which we send to that well function and then we're going to have an optional error which is going to be a server error like something went wrong in the database right so that's either a string or null And lastly we're gonna have an optional result which we represent as data which is going to be our T output. So with these two actions we've created generics which will work with any type of action we have. All we have to do is send in the input, which we expect the user to pass and the output, which we expect the user to receive, which can either be, you know, a success like data, which is gonna be some Prisma type, like a board or an error, which is a string or field errors which is going to be as you can see an object which has a specific key inside and then an array of errors inside like that and then let's actually create the safe action so export const create safe action is going to be a T input and T output and that's going to have a param of schema which is Z dot schema passed inside is going to be T input.
Besides the schema we're also going to have the handler itself, which will have the validated data, which is tinput. And we're going to return a promise, which is going to return the action state. And one more time inside, we're gonna pass in the T input and T output. Like that. And let's go ahead and return this.
And now let's write returnAsynchronous() function which accepts the data which is T input and returns a promise which is action state T input and T output. Let's go ahead and open this arrow function. And first let's validate our schema. So const validation result is going to be schema.safeParseData. And if we haven't received a success message so if exclamation point validation result dot success then let's go ahead and let's return one of the possible types for this action state in this case it's just going to return field errors.
So there's no error in the database and there's no data to return. We just have field errors to work with if this schema validation fails using Zod. So let's return the field errors. You can see how we already have a typescript here and we're going to return validation result dot error dot flatten dot field errors and we're going to manually write as field errors which holds the t input inside So let me just zoom out quickly so you can see it in one line like that. And then outside of this if function we're gonna go ahead and return the handler which gets the validated data.
So validation result dot data like that. Perfect so we finished our safe action handler and now we're going to go ahead and modify the function to actually be able to well be used by this create safe action. So let's go inside of our actions folder and let's go ahead and create a new folder called create-board and inside first let's create schema.ts and inside let's import Zod and let's write const create board to be Z.object and let's render well let's pass in what we expect from the form to come here. In our case for now, that's just going to be a string, which is a title, of course, and we're going to pass in the required error to be title is required. We're also going to pass in the invalid type error to be title is required as well.
And then let's add a minimum value of 3 and let's pass in the message inside to be title is too short. Like that. Perfect. So we now created our create board schema and let's also immediately export that and now let's create the file which is going to hold our types so input and output type inside of this folder create a new file types.ts on the same level as schema and in here let's go ahead and let's import Zod again Let's go ahead and let's import board from Prisma Client. So that's going to be our expected output.
Let's go ahead and import action state from lib createSafeAction. And finally, let's import the actual schema, which is createBoard from ./.schema and then simply export type input type to be z.info type of createBoard and let's export type return type to be action state which accepts the input type and the return type which is board like that. And now we can finally create our handler function. So that's actually going to be this create board so let's just copy it and paste it inside of this folder like that and just ignore all the errors for now and let's rename it to be index.ts like that And now let's go ahead and modify it slightly. So I think it's actually easier for us to remove everything from here.
And let's start by marking this as use server. And then let's write const handler to be an asynchronous function, which accepts the data, which is going to be the input type which we can now get from ./.types because we are in the same folder right here and the return type is going to be a promise which accepts the return type again from ./.types And let's go ahead and open this error function. And first thing that I want to check if we have user ID or not from out.clerk.next.js. So we're already going to start implementing the authentication inside of this handler, which you can imagine as an API route. So if we don't have a user ID, in that case, let's just go ahead and return an error.
And you can see how we have type safety here, right? Because we return this return type which can have field errors, error or data which is everything we defined inside of these types right here and because we wrapped it inside of action state from create safe action you can see that that's exactly what we expect. So from our server actions now we can only return specific items. So let's go ahead and write this error to be unauthorized. Great!
And now outside of here Let's go ahead and let's extract the title from this data, which is already going to be validated at this point, because we're gonna wrap it in safe action. And if you take a look at our safe action, once we pass in the schema, and once we pass in the actual data, you can see that it is validated right here so we are already gonna have some field errors and we don't have to do that every single time we do it here. Perfect! So we can safely extract the title from the data now and you can see how when I hover it expects the title in here And now let's go ahead and let's simply write let board like that and let's open a try and catch block. Inside of the try block let's simply write board to be equal to await db.board.create and let's also import the db from at slash lib db.
I'm just going to move it here. So let's go ahead and give it data to just be title. Great! And now let's go ahead inside of the catch function and let's get our error and if that happens we're going to simply return the error and that's going to be failed to create. Or you can write database error or internal error something like that.
Perfect and now let's go ahead and go further so if this catch did not happen in that case let's revalidate the path using next cache So I'm just going to move it here. Let's revalidate the following path. So open backticks slash board slash board ID which we just created. So we don't have this route yet but we are revalidating it for future cases because that's where we are going to redirect and then let's simply return data board like this. Perfect.
And then last thing we have to do is export const create board to be create safe action which I just imported from add slash lib createSafeAction and in the first argument let's pass in the createBoard schema so make sure you import that from .slash schema as I did right here and the second argument is going to be the handler like that and there we go, no errors. So now this createBoard is going to be a promise which is going to return our title well, it's going to return our board, right? But right now that's only the title. Perfect. What we have to do now is create a hook which is going to be able to accept this save action and give us all those useful callbacks like on success, on complete, etc.
So let's go ahead and let's just save this form it doesn't matter if you have an unsaved form just save it. It's gonna have an error but it doesn't matter because we're gonna go back and fix it. Now let's go inside of hooks and create a new file useaction.ts and first thing I want to do is I want to import use state and use callback from react itself and then let's go ahead and let's import action state and field errors from s slash lib slash create safe action and then let's create a type action to accept the T input the output, and a data which is the input and it returns a promise which is an action state which we just imported and that uses the T input and T output. Let me just zoom out for a second so you can see it in one line like that. Make sure you have that.
Great. And then let's create the interface to what we expect from this hook. So interface use action options is going to work with the output. It's going to have an optional on success handler which is going to give us the data which is a type of the output and let's just not misspell output and returns a void. Then we're going to have onError, which is also an optional callback, which gives us the server error and we can then work with it if we want to do that.
And onComplete, which is simply going to be a finally function so nothing in the params here and now let's finally write export const useAction which is going to accept the tInput, the tOutput. The first prop is going to be action, which is a type of action, which we defined right here. Whoops. And inside of that action, we have to pass in the T input and T output so let's give it that T input and T output besides that we're gonna have options which are gonna be use action options and works with T output and by default that's going to be an empty object so let's open this arrow function like this and let's just not misspell the output like that and also I forgot the equal sign here all right and first let's go ahead and let's create a state for field errors and let's create a set field errors. That's going to be working with useState and that useState is going to have a type of field errors and a generic inside T input.
Great. And by default, let's give it undefined. And besides T input, it's also, sorry, besides field errors, give it a pipe of undefined, so that's also a type of, a type of, well, prop it can work with. Great, so just make sure you have it written like that. Besides field errors we're also going to have the actual error state from the database so set error and inside use state which is a type of string or undefined and let's give it an undefined by default and const data setData which is again useState which works with the output or undefined and by default it is undefined.
And lastly we're gonna have is loading and set is loading which is used state and it's going to be a boolean which by default is false. Great so now we have this fields here and we can work with them. So let's write const execute to be use callback which is going to be an asynchronous function which accepts the input which is T input and right on the start we're going to set isLoading to be true and then we're going to open the try and catch block So let's try and get the result from the handler. So const result is await action and passing the input which is a type of T input as you can see here. If we don't have any result, just break the function.
And then if we have result field errors in that case let's set field errors to be result field errors if we have result dot error like a server error in that case set error to be result.error. And let's go ahead and write the last one so if result.data in that case set data to be result.data And while we are here let's also assign some callbacks. So we can do that for the error right here. Let's write options.onError?.result.error. And we're gonna do the same thing but for the data here so options on success result.data great and now outside of this try and catch block so let's add the catch block here and let's go ahead sorry not the catch block the finally block right so just like that.
We're gonna set isLoading to be false because it finished and options on complete and just execute it like that. Perfect. So now let's go ahead and let's pass in the action and the options. There we go. Perfect.
So let's take a look at it again. So we have this execute function which we are going to call inside of our components which is an asynchronous function which accepts the input which we have and we send that input inside of our action and then that action is going to run it through our action state and make sure it's validated. If for any reason we don't have any result there's nothing left to do with this action. We're not going to have any callbacks because obviously something went wrong out of our reach. If we have field errors it means that something went wrong with the validation and we don't have a callback for that because well we don't really need one we're simply going to extract the field errors and then we're gonna pass them to the components which need them.
If we have a server error we're going to set the error and we're also gonna add a options callback on error because that's not the type of error which we are going to render under the input field. That would be an error which we would render inside of a toast, right? Like something went wrong or internal error. And finally if we have result data it means that it successfully created the record. And then we're gonna add a callback on success so we can work with that record, like redirecting to a specific ID of that record or simply showing a success message as well.
And finally we're gonna have onComplete which well serves as the finally prop nothing more nothing less. And last thing we have to do is actually return all of those. So return execute field errors error and data and is loading. Great, so we just finished our use action handler. So just a quick reminder if any of this feels too much for you or you feel like you've made some mistakes you can always visit my github and take a look at this big files which we just created which are this use action and this lib create safe action right here as well as the actions folder where I have my index, schema and type.
So you can always go ahead and check them in my original source code. Perfect. So what we have to do now is we actually have to use that action inside of our form. So let's go back inside of our app folder, platform, dashboard, organization, organization ID. And in here, we should have the form where we are using this use form state to get the create function.
So we can now remove these two items from here and we can remove this create instead what we're going to do is we're going to import create board like that and we don't need the button or use form state and let's go ahead and let's do the following so we're gonna write const use action from hooks use action which we just created and inside we're gonna go ahead and pass the create board action like that and then from here we're gonna be able to extract the execute function as well as the field errors. So let's replace this state errors with those field errors and let's go ahead and write const onSubmit to accept form data which is a type of form data and we're gonna pass that inside of this action here and then let's extract the title to be formData.getTitle as string and well that's it for now that's the only field we have in the database and all we have to do is call the execute function and pass in the title just like that and the cool thing we can do now is call this callbacks so just add a comma and open up an object and in here as you can see we have the onComplete, onError, onSuccess so let's call the onSuccess let's get the data and let's console.log the data success like that let's add onError here which will get our error and in here console error, error.
Perfect. So let's try that out now. So I'm gonna go ahead and I'm gonna refresh my app here and I'm gonna add test, click submit and there we go you can see that my callback is working and I have the immediate access to this record which was just created in my callback. Perfect, so let's try it out and let's throw an error for example. Well actually let's do the quicker thing which is just entering a very short name like T.
There we go, you can see that it works as well. Perfect, so this field errors is working. And now let's go back inside of our actions create board index right here and inside of here let's throw new error it doesn't really matter let's just throw an error like that and let's see if that's going to give an error inside of our console, which is later going to be our well toast notification that something is wrong. So once I click submit, there we go. I have an error failed to create, which is the exact error which we send from the handler right here.
As you can see in the catch function we send an error failed to create and then we have access to it inside of this use action hook on error callback. Perfect! So you just flawlessly executed our vision which we had right here. So we combined our server action with three individual files all that so we can use it in a reusable way using use action. So this was my idea of creating a kind of a clone of use mutation which react query offers us which we are used to.
Of course this is my first time working with server actions and I just kind of experimented with this. If you have any criticism I would be very happy to hear it especially if it's optimization wise or security wise that would be great and you know your general opinions about this hook feel free to leave them in the comments below. Great great job.