Route and Controller Setup
A new POST route is created for article creation under an admin prefix. The controller defines a payload struct matching form inputs and binds incoming request data to it using the frameworkâs request binding utilities.
Article Creation Logic
The controller maps the validated payload to a model-layer article struct and calls a create function with the database connection. After successful creation, the user is redirected using an HTTP 303 status code.
Base Validation with go-playground/validator
A validator instance is initialized and configured to validate struct fields using tags. Standard rules such as required, min, max, gt, and omitempty are applied to enforce constraints without hitting the database.
Custom Publish Validation Rule
A custom struct-level validation ensures that when an article is marked as published, both excerpt and content must be non-empty. This rule is registered with the validator to prevent publishing incomplete articles.
Domain Error Handling
A custom domain validation error is defined and combined with validation errors using error chaining. The controller inspects the error tree to distinguish validation failures from other types of errors.
UI Feedback Integration
A reusable fieldProp structure is introduced to pass both field values and error messages to the view layer. Form components are updated to display validation errors dynamically beneath input fields.
Mapping Validation Errors to Form Fields
A helper function converts validator errors into a map of field properties. It preserves user input, associates errors with specific fields, and generates user-friendly error messages for display.
End-to-End Validation Flow
On form submission, validation errors are extracted and mapped back to the UI instead of returning a generic server error. Valid submissions result in correctly saved draft or published articles, ensuring consistent data integrity.
Let's take our validation and creating an article. So we're gonna start like we always do by creating a route. And we're gonna create a route called create article. And this one is going to admin prefix plus slash article. Give that a save. Then we jump out into the router. And here we're gonna create our first host request because we're creating a new resource. And this one,
use the create article route. And it will use to create article controller that does not exist yet. So jump into controller. Let's go down here. Let's copy this. Let's say create article. And now we need to define a payload. And the payload is going to be what we are sending from the
from the UI. So we're going to type, article form, payload. And here we expect a title. We expect an excerpt, that's going to be a string, a state that's going to be a string, and content that's going to be a string. Now, Ego provides us with some helper methods so that we can easily grab out these values. I will show you when we submit from the browser what it actually sends to the back end.
But we can add these fields or struct tags here. So it matches the name that we provided to the field in the front end. And there we set article title. Now I can grab all of this, go down here and paste it, and then it's article excerpt, article.
state, or did we just call it state? Let me just check here really quick. Admin, we called it, we called it the article state, we called it just state. Okay, so state and then article content. Now we can create a new variable here called payload as of type
article form payload, and then we use this if error, easy, and then we simply bind, bind, pass, program, and query programs into the request body, right? So, I point that to the payload, and we say if error does not equal nil, then we simply, for now, just return the error. So this makes so that echo grabs out, takes the post request,
looks for these fields that we have provided here in the request and transforms it into the struct that we have created and then we can interact with it. So we have the same payload and we can see we have title, which is what is coming from the frontend. Then we're gonna say blank error equals models create article. Here we pass the request, we also pass the DB
connection and then we need to create this article data structure that we have created in the model layer. Alright, I'm going to use code action here to just fill it out. Okay, of course we need to initialize error. So title simply becomes payload title and payload excerpt payload
Don't we have content? We have content. And now we need to look at the value that gets in from our draft or state radio buttons. So if we say payload and state equals to publish, this evaluates to true and we will release the article when we create it. And let me just verify that
The state was, the value was published. All right, let's jump back. Again, simply return the hour and we don't really need, so we return the new model that's created. We don't really need this because we are gonna, for now redirect back to the admin home page. When we have the update page, we're gonna redirect to that so we can see what we created. So let's just ignore the article that's returned and say,
Easy, redirect. And we're going to say HTTP. And we can use these status codes here. So we can see this value is 303. And it's just some values that exposed. The values are standardized from HTTP spec, I believe. But the values is coming from the standard library.
and then we just route back to the admin page. That's it. Now we can technically submit our article and have it be stored in the database, but we also want to validate that the values are correct or in the shape that we want before we store anything in the database. If something is not in a state that we expect, we want to provide some feedback to ourselves so we can make the adjustments and then save a correctly formed article.
Great, so I'm in the models.go file here, and we need to define a new variable that we can interact with to do our validation. So I'm gonna create a new variable here called validate, and we're gonna create a function called setUpValleyData. That's gonna return us our, no, sorry, this is, I'm getting ahead of myself here, it doesn't exist yet, right? So we just call this function,
And then we say, func setup validator. That will return us a validator, validate, give that a save and we need to import this library as well. So let me just go get that package. We start the lsp and we should have, yes. In here, we say v equals validator.new and validator.
with required struct in Avald. And then we could just return this directly, but we're gonna make some modifications, our own custom validation rule that we're gonna add to this returned validate object here, struct, but that is getting a little bit ahead of ourselves. So, what goes on here? Well, we are gonna use this library here called
a validator and if I jump into the browser. This is the repository and you can see like 19,000 stars. This is quite popular. What it does is to allow us to add some text to our struct text like we did with the form and then run it through this validator and then it will simply just validate that. So, if you put something like
We have a field here that's supposed to be an IPv4 address. If we add this field tag, it will validate the value and state whether or not this is a valid IPv4 address. So it takes care of a lot of the validation that we might write by hand ourselves. And you can do a bunch of things in here. So definitely check out the go-desk-playground-validator for what you can actually access.
Great. Now we have this validate variable. So we can go into our article and then find our article data here. And what we want to do is say validate. And then you're going to say title is always going to be required. It has to be a minimum of five characters and a maximum of a hundred characters, which is what we specified in our migration. And then we're just saying, Hey,
you need to make this at least five characters. We always have a valid title, even if it's in draft, not. That is simply because if you have something in draft with no title, it's really hard to know what you have done. So a little help to ourself. Then we're gonna say validate, excerpt. We don't necessarily need an excerpt if something is in draft. So we can say omit, empty.
Did I spell this correctly, omit MTS? And we're gonna say that the max value can be 255 characters, which is again what we also stated in the database, but doing it this way, we don't need to hit the database, we will know beforehand. For the content, we are just gonna say greater than, let's say 10 characters.
Again, you can do whatever you want here. This is only going to apply when we actually publish an article, right? Because this is the extra layer of validation we're going to add, because we're not going to add anything to the publish field here. But we will add a custom validation function that goes in and say, if this is set to true, then we validate that these two fields here is also true. So this is just a little
check for ourselves so we don't actually, extensively release something, or release an article with nothing in it. It could be one, it could be five, whatever you want. Great. Now, final thing is we go down here and we say, if error, and then we use the valid date, that we just created the variable, and then we say struct. We pass the data, which is from the article data here. The error does not equal null.
And then we return an empty article struct. And now we need to return an error. And we could just return an error here. But we might also have an error here that is different from this one. So before we go further, I'm going to go back into models. And I'm going to create another variable here called error domain validation.
set that to errors new so we can create our own custom errors and say the provided payload, or say provided data, that's what we call it, failed validation. Now we, in the controller, can check for the type of error that we get back from the model layer and then do what we need to do depending on whatever we get back. All right, so the,
This one now becomes errors join. And I want to join so that we have both the indicator here of what went wrong and then the actual error. So I will show in a second where we can check from the chain of errors. So this is just errors. It's just a big long chain. And we can check if something like this exists in the chain. And then we can also pull out this other error from the chain.
This just indicates to ourselves that, hey, the domain validation failed. Now for our custom error. So I'm back in the model.go file here, and we're going to create a function called publishArticleValidation. That's going to take in an argument called sl of type validator struct level. And in here, we're going to say data equals to sl current
interface, or not internal, interface. And then we can use this little notation here to pass it into an article data struct. So this is the struct we have in our articles. So go model. And this just gives us access to the pass data from the validator library packets that we're currently interacting with. Great. Then we're going to say data.
publish. So if this is true, we are going to check that data dot excerpt is equal to an empty string. You're also going to check if content is equal to an empty string. And if they are, we're going to say SL report error and say data excerpt. And what do we then so data report error, data excerpt, and we're going to say there
So actually, let me show you the naming. So we have the field name, struct field name, tag, and param. So we're going to specify excerpt, and we're going to say excerpt one more time. We're going to call this required when publish, and finally just going to pass an empty string. We're going to do the same down here. We are just going to call it
content and content again required when published. The reason why we check for if this is true and then check if there are empty strings is that we have this omit-empty thing in place so that we can save drafts without content and without an excerpt. However, this omit-empty goes away when we actually add content to it. So we ensure that we
have content, and then when we have some content, the rules get applied. So if we apply something to, let's say, or we add something to the excerpt, and we do more than 255 characters, the rule will automatically apply in the understruct. So the article data struct here. And the same for content. But if we pass nothing, then we have this extra check that says, OK, if something is said to publish, then this check runs so we ensure that we never
get into this state where we release an article that has no data. All right. Then we need to register. So we're gonna say V register. Register struct validation and gonna be publish. And we're also gonna pass the struct that this should apply to.
and I am doing something wrong here. That's because we need to pass it an empty struct. There we go. So now whenever we are dealing with an article data struct and we run the, let me go back in here and whenever we run the validate.struct on that type, this published article validation will run and we ensure we don't end up in an invalid state.
Wow, that's a lot. But we still need to deal with the visual feedback to ourselves if something does not pass validation. So we're going to expand upon our components so that we can pass in the value that we gave it in the first place and then the error so we can show it in the UI that, hey, something went wrong. You need to fix this. So in admin.temple, we are going to go down to our input fields here.
I'm going to specify a new type that we're going to export called field prop. That's a struct that we have our field called value string and error string. Then we make our input field accept this prop. So let's just say prop field prop. Then the value becomes prop dot no.
Yes, it becomes prop.value. And then beneath the input field, we're going to add if field prop.error is not an empty string. Ah, you call it prop, right? And then we're going to add a span.
Let's showcase the error. Error. I need to fix this. There we go. Give that a save. And now we just need to add a class of text error. And then we're gonna truncate it. If it becomes too long, the text will get cut off. And then we can deal with
our error message if we need to, but this is a really good, really good starting point. Okay, we need to do this for our field prop, sorry for our text field where we again need to use the prop value. And then we can simply just grab this right here so that it's basically the same as before, but in our
new article page here, we need to accept a field prop. So what we need to do is to create a map called type field props of type map string field prop. And then we say props field
props. And finally, we can go in here and pass it. So we're going to say props. And we know the name here is going to be article title. Come on. There we go. And then repeat this process for the other ones. So article excerpt.
are going to be excerpt. And the final one, the content. That should be article content. Come on. All right. So now we just need to pass an empty version of this when we visit the page and everything will work as before. But now when we submit, we have the tools in place to actually validate the input data.
Okay, I'm back in the controller.go file and we can see we now have an error. So we need to pass it these fill props that we just created. And we are just going to pass an empty version here because we have no props right now. Then we need to go out.
and say go mod tidy, we have everything in place, good. Let's say just run, does it run, we do, okay. And now if we go back into a new article page and give it a refresh, nothing happened, just as we expected. Let's try and create an article, test article. And we should be able to just create it like this. So let's say save and see what happens.
Oh, we haven't, of course, haven't updated the routes that we actually sent the request to. So to do, not edit, but this one is going to be routes, create article. And please stop doing that. Go out again, just run.
Give it a refresh and let's say test article. And you can see we have test article here that is in draft and is created at the time of recording here. We of course can edit yet, but we can go in and let's actually try and say my second article and say publish. Let's see what happens.
have an internal server error, and that's because right now in our controller, in our controller, when we have an error, we just return an error, so nothing happens. So what we need to do is we need to create a function that will take in the validation error if there are any, and then map them to this field props, and then return the page back with those values filled out. So let's deal with that, and then we can
we can see the errors getting shown in our UI. So since we're gonna be doing this for the updates functionality as well, I'm gonna create a function called funk to article field props. That will return us views, field props, and it will take in a payload of article form payload and some validation
validation errors. That's going to be value data and validation. I need to import the validator and then we should have validation errors. Great. Then we say title equal views and then it's just field prop. The value will be payload title.
I am doing this wrong because this is a map, of course, like this. Give that a save. And then we repeat this for the excerpt, excerpt, and for the content, content. And now we can say for blank field error, range, validation, errors,
take the field name, we get that from strings, lower, and we have no two lower, two lower, gonna field error, fields, right? So we simply just get some, like the fields from the validation errors that get exposed through the validation package that we are using. Then we simply take a switch statement here,
Let's watch switch field name, let's say case title and then we say title that we just created. Error equal to title must be between five and one hundred characters. Maybe with that and we need for the
excerpt this and say excerpt and excerpt cannot be longer than 255 characters finally the content content and content let's just say content cannot be
empty when publishing. So you can see how again, this separation of concern that we have some, we've just in the model layer validate that is this data that what we want it to be. And then in the views layer, we simply just display what is happening. And then here in the controller, we combine everything together. So the two can work without knowing about each other. Great. Finally, we just say return views, fields,
props and then we say article title and pass it the title. Do that two more times for article excerpt. Pass in excerpt and article contents. Great. So we use our payload.
We use the validation errors that we set up from the validator package. We provide the value that we receive so we can show that value again in the UI. We then range over all of the errors that was returned to us. We lowercase the field name to be consistent. And then we simply map that with a switch statement, provide a helpful error message, and then return the props. All right, we are almost done.
do the validation error extraction. So this error package that we used to create an error, we can also use to extract an error. So we can use this errors as function. So finds the first error in the errors tree that matches the target. And then it sets that target to the error value and it turns true. Great. So that means we also need
to create this variable here called validation errors. That's gonna be validator, validation errors. Then we pass the error returned and the target is validation errors. And if, so we're actually gonna say if this is not true, then
We simply return an error, but if this evaluates to true, we need to be saying return, and then we can actually grab from up here, and then pass in the, I lost my four here, two article props validation errors. Two field props.
This, this, this. And what do we need to pass more? Ah, we need to pass the payload. Great. Let's jump out and run one more time. Let's see what happens. Go back, give it a refresh, say my second article one more time and say to publish.
And now you can see, since we set it to publish, excerpt cannot be longer than 255 characters. Content cannot be empty when publishing. The only thing that's really missing here is that we should probably also keep the state that we want. But we're going to leave that for now. This is very basic how we do validation. And now, if we provide some values here, I don't know what this is.
article and then we do hello, hello world and my second article. Now can we save it can and do we see we see my second article here published if we go to home my second article and it's also formatted correctly. Great. But this
right here. It's not really a nice way to write markdown. So what we're going to be adding in the next episode is what you see is what you get editor that will make this experience a lot nicer for us and provides us with some things we can talk about, like bold text or underlined or different headaches, just like you see in a word or something similar.