Chapters

Hide chapters

Server-Side Swift with Vapor

Third Edition · iOS 13 · Swift 5.2 - Vapor 4 Framework · Xcode 11.4

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section I: Creating a Simple Web API

Section 1: 13 chapters
Show chapters Hide chapters

21. Validation
Written by Tim Condon

In the previous chapters, you built a fully-functional API and website. Users can send requests and fill in forms to create acronyms, categories and other users. In this chapter, you’ll learn how to use Vapor’s Validation library to verify some of the information users send the application. You’ll create a registration page on the website for users to sign up. You’ll then validate the data from this form and display an error message if the data isn’t correct.

The registration page

Create a new file in Resources/Views called register.leaf. This is the template for the registration page. Open register.leaf and replace its contents with the following:

#extend("base"):
  #export("content"):
    <h1>#(title)</h1>

    <form method="post">
        <div class="form-group">
        <label for="name">Name</label>
        <input type="text" name="name" class="form-control"
        id="name"/>
        </div>

        <div class="form-group">
        <label for="username">Username</label>
        <input type="text" name="username" class="form-control"
        id="username"/>
        </div>

        <div class="form-group">
        <label for="password">Password</label>
        <input type="password" name="password"
        class="form-control" id="password"/>
        </div>

        <div class="form-group">
        <label for="confirmPassword">Confirm Password</label>
        <input type="password" name="confirmPassword"
        class="form-control" id="confirmPassword"/>
        </div>

        <button type="submit" class="btn btn-primary">
        Register
        </button>
    </form>
  #endexport
#endextend

This is very similar to the templates for creating an acronym and logging in. The template contains four input fields for:

  • name
  • username
  • password
  • password confirmation

Save the file. Next, in Xcode, open WebsiteController.swift and, at the bottom of the file, add the following context for the registration page:

struct RegisterContext: Encodable {
  let title = "Register"
}

Next, below logoutHandler(_:), add the following route handler for the registration page:

func registerHandler(_ req: Request) -> EventLoopFuture<View> {
  let context = RegisterContext()
  return req.view.render("register", context)
}

Like the other routes handlers, this creates a context then calls render(_:_:) to render register.leaf.

Next, create the Content for the POST request for registration, add the following to the end of WebsiteController.swift:

struct RegisterData: Content {
  let name: String
  let username: String
  let password: String
  let confirmPassword: String
}

This Content type matches the expected data received from the registration POST request. The variables match the names of the inputs in register.leaf. Next, create a route handler for this POST request, add the following after registerHandler(_:):

// 1
func registerPostHandler(
  _ req: Request
) throws -> EventLoopFuture<Response> {
  // 2
  let data = try req.content.decode(RegisterData.self)
  // 3
  let password = try Bcrypt.hash(data.password)
  // 4
  let user = User(
    name: data.name,
    username: data.username,
    password: password)
  // 5
  return user.save(on: req.db).map {
    // 6 
    req.auth.login(user)
    // 7
    return req.redirect(to: "/")
  }
}

Here’s what’s going on in the route handler:

  1. Define a route handler that accepts a request and returns EventLoopFuture<Response>.

  2. Decode the request body to RegisterData.

  3. Hash the password submitted to the form.

  4. Create a new User, using the data from the form and the hashed password.

  5. Save the new user and unwrap the returned future.

  6. Authenticate the session for the new user. This automatically logs users in when they register, thereby providing a nice user experience when signing up with the site.

  7. Return a redirect back to the home page.

Next, in boot(routes:) add the following below authSessionsRoutes.post("logout", use: logoutHandler):

// 1
authSessionsRoutes.get("register", use: registerHandler)
// 2
authSessionsRoutes.post("register", use: registerPostHandler)

Here’s what this does:

  1. Connect a GET request for /register to registerHandler(_:).
  2. Connect a POST request for /register to registerPostHandler(_:data:).

Finally, open base.leaf. Before the closing </ul> in the navigation bar, add the following:

<!-- 1 -->
#if(!userLoggedIn):
  <!-- 2 -->
  <li class="nav-item #if(title == "Register"): active #endif">
    <!-- 3 -->
    <a href="/register" class="nav-link">Register</a>
  </li>
#endif

Here’s what the new Leaf code does:

  1. Check to see if there’s a logged in user. You only want to display the register link if there’s no user logged in.
  2. Add a new navigation link to the navigation bar. Set the active class if the current page is the Register page.
  3. Add a link to the new /register route.

Save the template then build and run the project in Xcode. Visit http://localhost:8080 in your browser. You’ll see the new navigation link:

Click Register and you’ll see the new register page:

If you fill out the form and click Register, the app takes you to the home page. Notice the Log out button in the top right; this confirms that registration automatically logged you in.

Basic validation

Vapor provides a validation module to help you check data and models. Open WebsiteController.swift and add the following at the bottom:

// 1
extension RegisterData: Validatable {
  // 2
  public static func validations(
    _ validations: inout Validations
  ) {
    // 3
    validations.add("name", as: String.self, is: .ascii)
    // 4
    validations.add(
      "username", 
      as: String.self, 
      is: .alphanumeric && .count(3...))
    // 5
    validations.add(
      "password", 
      as: String.self, 
      is: .count(8...))
  }
}

Here’s what this does:

  1. Extend RegisterData to make it conform to Validatable. Validatable allows you to validate types with Vapor.
  2. Implement validations(_:) as required by Validatable.
  3. Add a validator to ensure RegisterData’s name contains only ASCII characters and is a String. Note: Be careful when adding restrictions on names like this. Some countries, such as China, don’t have names with ASCII characters.
  4. Add a validator to ensure the username contains only alphanumeric characters and is at least 3 characters long. .count(_:) takes a Swift Range, allowing you to create both open-ended and closed ranges, as necessary.
  5. Add a validator to ensure the password is at least eight characters long. Currently, it’s not possible to add a validation to two different properties. You must provide your own check that password and confirmPassword match.

As you can see, Vapor allows you to create powerful validations on models or incoming data. In registerPostHandler(_:), add the following at the top of the method:

do {
  try RegisterData.validate(content: req)
} catch {
  return req.eventLoop.future(req.redirect(to: "/register"))
}

This calls validate(content:) on RegisterData, checking each each validator you added previously. validate(content:) can throw a number of ValidationsErrors depending on the checks you added. In an API, you can let this error propagate back to the user but, on a website, that doesn’t make for a good user experience. In this case, you redirect the user back to the “register” page.

Build and run, then visit the “register” page in your browser. If you enter information that doesn’t match the validators, the app sends you back to try again.

Custom validation

Vapor allows you to write expressive and complex validations, but sometimes you need more than the built-in options offer. For example, you may want to validate a US Zip code. To demonstrate this, at the bottom of WebsiteController.swift, add the following:

// 1
extension ValidatorResults {
  // 2
  struct ZipCode {
    let isValidZipCode: Bool
  }
}

// 3
extension ValidatorResults.ZipCode: ValidatorResult {
  // 4
  var isFailure: Bool {
    !isValidZipCode
  }

  // 5
  var successDescription: String? {
    "is a valid zip code"
  }

  // 6
  var failureDescription: String? {
    "is not a valid zip code"
  }
}

Here’s what the new code does:

  1. Create an extension for ValidatorResults to add your own results.
  2. Create a ZipCode result that contains the result check.
  3. Create an extension for the new ZipCode type that conforms to ValidatorResult.
  4. Implement isFailure as required by ValidatorResult. Define what counts as a failure.
  5. Implement successDescription as required by ValidatorResult.
  6. Implement failureDescription as required by ValidatorResult. Vapor uses this when throwing an error when isFailure is true.

Next, at the bottom of the file add a new Validator for a zip code:

// 1
extension Validator where T == String {
  // 2
  private static var zipCodeRegex: String {
    "^\\d{5}(?:[-\\s]\\d{4})?$"
  }

  // 3
  public static var zipCode: Validator<T> {
    // 4
    Validator { input -> ValidatorResult in
      // 5
      guard 
        let range = input.range(
          of: zipCodeRegex, 
          options: [.regularExpression]), 
        range.lowerBound == input.startIndex 
          && range.upperBound == input.endIndex
      else {
        // 6
        return ValidatorResults.ZipCode(isValidZipCode: false)
      }
      // 7
      return ValidatorResults.ZipCode(isValidZipCode: true)
    }
  }
}

Here’s what the new validator does:

  1. Create an extension for Validator that works on Strings.
  2. Define the regular expression to use to check for a valid US zip code.
  3. Define a new validator type for a zip code.
  4. Construct a new Validator. This takes a closure which has the data to validate as the parameter and returns ValidatorResult.
  5. Check the zip code matches the regular expression.
  6. If the zip code does not match, return ValidatorResult with isValidZipCode set to false.
  7. Otherwise, return a successful ValidatorResult.

Finally, in the extension conforming RegisterData to Validatable, add the following to the end of validations(_:):

validations.add(
  "zipCode", 
  as: String.self, 
  is: .zipCode,
  required: false)

This add a validation to a property called zipCode sent in the request body. The property must be a String and match the zipCode validation you added above. However, setting required to false marks the property as optional. Vapor will validate it if it exists, but won’t throw an error if zipCode is not sent. This is useful as you don’t have a zip code property on your registration form yet!

Displaying an error

Currently, when a user fills out the form incorrectly, the application redirects back to the form with no indication of what went wrong. Open register.leaf and add the following under <h1>#(title)</h1>:

#if(message):
  <div class="alert alert-danger" role="alert">
    Please fix the following errors:<br />
    #(message)
  </div>
#endif

If the page context includes message, this displays it in a new <div>. You style the new message appropriately by setting the alert and alert-danger classes. Open WebsiteController.swift and add the following to the end of RegisterContext:

let message: String?

init(message: String? = nil) {
  self.message = message
}

This is the message to display on the registration page. Remember that Leaf handles nil gracefully, allowing you to use the default value in the normal case.

In registerHandler(_:), replace:

let context = RegisterContext()

With the following:

let context: RegisterContext
if let message = req.query[String.self, at: "message"] {
  context = RegisterContext(message: message)
} else {
  context = RegisterContext()
}

This checks the request’s query. If message exists — i.e., the URL is /register?message=some-string — the route handler includes it in the context Leaf uses to render the page.

Finally, in registerPostHandler(_:data:), replace the catch block with:

catch let error as ValidationsError {
  let message = 
    error.description
    .addingPercentEncoding(
      withAllowedCharacters: .urlQueryAllowed
    ) ?? "Unknown error"
  let redirect = 
    req.redirect(to: "/register?message=\(message)")
  return req.eventLoop.future(redirect)
}

When validation fails, the route handler extracts the description from the ValidationsError. Vapor combines all the errors into one description. The code then escapes the description properly for inclusion in a URL or provides a default message if the description is nil. It then adds the message to the redirect URL. Finally, it redirects the user back to the registration page. Build and run, then visit http://localhost:8080/register in your browser.

Submit the empty form and you’ll see the new message:

Where to go from here?

In this chapter, you learned how to use Vapor’s validation library to check a request’s data. You can apply validation to models and other types as well.

In the next chapter, you’ll learn how to integrate the TIL application with an OAuth provider. This lets you delegate login and registration to online services such Google or GitHub, allowing users to sign in with an existing account.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.