3

Form Validation and Processing in Go

 3 years ago
source link: https://www.alexedwards.net/blog/form-validation-and-processing
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Form Validation and Processing in Go

Last updated: 30th March 2020 Filed under: golang tutorial

In this post I want to outline a sensible pattern that you can use for validating and processing HTML forms in Go web applications. Over the years I've tried out a number of different approaches, but this is the basic pattern that I always keep coming back to. It's clear and uncomplicated, but also flexible and extensible enough to work well in a wide variety of projects and scenarios.

To illustrate the pattern, I'll run through the start-to-finish build of a simple online contact form.

So let's begin by creating a new directory for the application, along with a main.go file for our code and a couple of vanilla HTML templates:

$ mkdir -p contact-form/templates
$ cd contact-form
$ touch main.go templates/home.html templates/confirmation.html
File: templates/home.html
<h1>Contact</h1>
<form action="/" method="POST" novalidate>
  <div>
    <p><label>Your email:</label></p>
    <p><input type="email" name="email"></p>
  </div>
  <div>
    <p><label>Your message:</label></p>
    <p><textarea name="content"></textarea></p>
  </div>
  <div>
    <input type="submit" value="Send message">
  </div>
</form>
File: templates/confirmation.html
<h1>Confirmation</h1>
<p>Your message has been sent!</p>

If you're following along you'll also need to enable modules in the application root by running the go mod init command like so:

$ go mod init contact-form.example.com 
go: creating new go.mod: module contact-form.example.com

Once that's done, your directory structure should look like this:

.
├── templates
│   ├── confirmation.html
│   └── home.html
├── go.mod
└── main.go

Displaying the Form

Our application is going to provide three routes:

Method URL Path Handler Description GET / home Display the contact form POST / send Submit the contact form GET /confirmation confirmation Display a confirmation message after successful submission To handle the routing of requests we're going to use pat – a third-party router package which I've talked about before. But if you want to use an alternative router please feel free.

Let's go ahead and create a skeleton for the application:

File: main.go

package main

import (
  "html/template"
  "log"
  "net/http"

  "github.com/bmizerany/pat"
)

func main() {
  mux := pat.New()
  mux.Get("/", http.HandlerFunc(home))
  mux.Post("/", http.HandlerFunc(send))
  mux.Get("/confirmation", http.HandlerFunc(confirmation))

  log.Println("Listening...")
  err := http.ListenAndServe(":3000", mux)
  if err != nil {
    log.Fatal(err)
  }
}

func home(w http.ResponseWriter, r *http.Request) {
  render(w, "templates/home.html", nil)
}

func send(w http.ResponseWriter, r *http.Request) {
  // Step 1: Validate form
  // Step 2: Send message in an email
  // Step 3: Redirect to confirmation page
}

func confirmation(w http.ResponseWriter, r *http.Request) {
  render(w, "templates/confirmation.html", nil)
}

func render(w http.ResponseWriter, filename string, data interface{}) {
  tmpl, err := template.ParseFiles(filename)
  if err != nil {
    log.Println(err)
    http.Error(w, "Sorry, something went wrong", http.StatusInternalServerError)
  }

  if err := tmpl.Execute(w, data); err != nil {
    log.Println(err)
    http.Error(w, "Sorry, something went wrong", http.StatusInternalServerError)
  }
}  

This is fairly straightforward stuff so far. The only real point of note is that we've put the template handling into a render function to cut down on boilerplate code.

If you run the application:

$ go run .
2020/03/30 06:41:42 Listening...

And then visit localhost:3000 in your browser you should see the contact form being displayed (although it doesn't do anything yet!).

contact-1.png

Validating the Form

Now for the interesting part. Let's add some validation rules to this contact form, display the validation errors if there are any, and make sure that the form values get presented back if there's an error so the user doesn't need to retype them.

We could add the code for this inline in our send handler, but personally I find it cleaner and neater to break out the logic into a separate message.go file:

$ touch message.go
File: message.go
package main

import (
  "regexp"
  "strings"
)

var rxEmail = regexp.MustCompile(".+@.+\\..+")

type Message struct {
  Email   string
  Content string
  Errors  map[string]string
}

func (msg *Message) Validate() bool {
  msg.Errors = make(map[string]string)

  match := rxEmail.Match([]byte(msg.Email))
  if match == false {
    msg.Errors["Email"] = "Please enter a valid email address"
  }

  if strings.TrimSpace(msg.Content) == "" {
    msg.Errors["Content"] = "Please enter a message"
  }

  return len(msg.Errors) == 0
}

So what's going on here?

We've started by defining a rxEmail variable, containing a simple regular expression for validating the format of the email address in the form.

Then we define a Message struct, consisting of Email and Content fields (which will hold the data from the submitted form), along with an Errors map to hold any validation error messages.

We then created a Validate() method that acts on a given Message, which checks the format of the email address and makes sure that the content isn't blank. In the event of any errors we add them to the Errors map, and finally return a true or false value to indicate whether validation passed successful or not. In a large project you might want to break the validation checks into helper functions to reduce duplication.

This approach means that we can keep the code in our send handler fantastically light. All we need it to do is retrieve the form values from the POST request, create a new Message instance containing them, and call Validate(). If the validation fails we can re-render the contact form, passing back the relevant Message struct. Like so:

File: main.go

...

func send(w http.ResponseWriter, r *http.Request) {
	// Step 1: Validate form
	msg := &Message{
		Email:   r.PostFormValue("email"),
		Content: r.PostFormValue("content"),
	}

	if msg.Validate() == false {
		render(w, "templates/home.html", msg)
		return
	}

	// Step 2: Send message in an email
	// Step 3: Redirect to confirmation page
}

...

As a side note, in the code above we're using the PostFormValue() method on the request to access the POST data. This is a helper method which parses the form data in the request body (using ParseForm()) and returns the value for a specific field. If no matching field exists in the request body, it will return the empty string "".

For large request bodies, you might also want to consider using the Gorilla Schema package to automatically decode the form values in to a struct, instead of assigning them manually like we have done in the code above.

Anyway, let's now update our home.html template so it displays the validation errors (if they exist) above the relevant fields, and repopulate the form inputs with any information that the user previously typed in:

File: templates/home.html

<style type="text/css">.error {color: red;}</style>

<h1>Contact</h1>
<form action="/" method="POST" novalidate>
  <div>
    {{ with .Errors.Email }}
    <p class="error">{{ . }}</p>
    {{ end }}
    <p><label>Your email:</label></p>
    <p><input type="email" name="email" value="{{ .Email }}"></p>
  </div>
  <div>
    {{ with .Errors.Content }}
    <p class="error" >{{ . }}</p>
    {{ end }}
    <p><label>Your message:</label></p>
    <p><textarea name="content">{{ .Content }}</textarea></p>
  </div>
  <div>
    <input type="submit" value="Send message">
  </div>
</form>

Let's try this out. Go ahead and run the application:

$ go run .
2020/03/30 08:41:42 Listening...

And try submitting an invalid form. You should find that the form is redisplayed along with the relevant data and validation errors like so:

contact-2.png

Sending the Contact Form Message

Great! That's now working nicely, but our contact form isn't very useful unless we actually do something with it. Let's add a Deliver() method to our Message which sends the contact form message to a particular email address. In the code below I'm using the go-mail/mail package and a mailtrap.io account for email sending, but the same thing should work with any other SMTP server.

File: message.go

package main

import (
  "regexp"
  "strings"

  "github.com/go-mail/mail"
)

...

func (msg *Message) Deliver() error {
  email := mail.NewMessage()
  email.SetHeader("To", "[email protected]")
  email.SetHeader("From", "[email protected]")
  email.SetHeader("Reply-To", msg.Email)
  email.SetHeader("Subject", "New message via Contact Form")
  email.SetBody("text/plain", msg.Content)

  username := "your_username"
  password := "your_password"

  return mail.NewDialer("smtp.mailtrap.io", 25, username, password).DialAndSend(email)
}  

The final step is to head back to our main.go file, add some code to call Deliver(), and issue a 303 See Other redirect to the confirmation page that we made earlier:

File: main.go

...

func send(w http.ResponseWriter, r *http.Request) {
	// Step 1: Validate form
	msg := &Message{
		Email:   r.PostFormValue("email"),
		Content: r.PostFormValue("content"),
	}

	if msg.Validate() == false {
		render(w, "templates/home.html", msg)
		return
	}

	// Step 2: Send contact form message in an email
	if err := msg.Deliver(); err != nil {
		log.Println(err)
		http.Error(w, "Sorry, something went wrong", http.StatusInternalServerError)
		return
	}

	// Step 3: Redirect to confirmation page
	http.Redirect(w, r, "/confirmation", http.StatusSeeOther)
}

...

So long as your SMTP server account credentials are set up correctly, you should now be able to successfully submit the contact form and you should see the confirmation message below in your browser.

contact-3.png

If you enjoyed this blog post, don't forget to check out my new book about how to build professional web applications with Go!

Follow me on Twitter @ajmedwards.

All code snippets in this post are free to use under the MIT Licence.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK