Home iOS & Swift Books Server-Side Swift with Vapor

8
Controllers Written by Tim Condon

In previous chapters, you’ve written all the route handlers in routes.swift. This isn’t sustainable for large projects as the file quickly becomes too big and cluttered. This chapter introduces the concept of controllers to help manage your routes and models, using both basic controllers and RESTful controllers.

Note: This chapter requires that you have set up and configured PostgreSQL. Follow the steps in Chapter 6, “Configuring a Database”, to set up PostgreSQL in Docker and configure the Vapor application.

Controllers

Controllers in Vapor serve a similar purpose to controllers in iOS. They handle interactions from a client, such as requests, process them and return the response. Controllers provide a way to better organize your code. It’s good practice to have all interactions with a model in a dedicated controller. For example in the TIL application, an acronym controller can handle all CRUD operations on an acronym.

Controllers are also used to organize your application. For instance, you may use one controller to manage an older version of your API and another to manage the current version. This allows a clear separation of responsibilities in your code and keeps code maintainable.

Getting started with controllers

In Xcode, create a new Swift file to hold the acronyms controller. Create the file in Sources/App/Controllers and call it AcronymsController.swift.

Route collections

Inside a controller, you define different route handlers. To access these routes, you must register these handlers with the router. A simple way to do this is to call the functions inside your controller from routes.swift. For example:

app.get(
  "api",
  "acronyms",
  use: acronymsController.getAllHandler)

This example calls getAlllHandler(_:) on the acronymsController. This call is like the route handlers you wrote in Chapter 7. However, instead of passing a closure as the final parameter, you pass the function to use.

This works well for small applications. But if you’ve a large number of routes to register, routes.swift again becomes unmanageable. It’s good practice for controllers to be responsible for registering the routes they control. Vapor provides the protocol RouteCollection to enable this.

Open AcronymsController.swift in Xcode and add the following to create an AcronymsController that conforms to RouteCollection:

import Vapor
import Fluent

struct AcronymsController: RouteCollection {
  func boot(routes: RoutesBuilder) throws {
  }
}

RouteCollection requires you to implement boot(routes:) to register routes. Add a new route handler after boot(routes:):

func getAllHandler(_ req: Request) 
    -> EventLoopFuture<[Acronym]> {
  Acronym.query(on: req.db).all()
}

The body of the handler is identical to the one you wrote earlier and the signature matches the signature of the closure you used before. Register the route in boot(routes:):

routes.get("api", "acronyms", use: getAllHandler)

This makes a GET request to /api/acronyms call getAllHandler(_:). You wrote this same route earlier in routes.swift. Now, it’s time to remove that one. Open routes.swift and delete the following handler:

app.get("api", "acronyms") { 
  req -> EventLoopFuture<[Acronym]> in
  Acronym.query(on: req.db).all()
}

Next, add the following to the end of routes(_:):

// 1
let acronymsController = AcronymsController()
// 2
try app.register(collection: acronymsController)

Here’s what this does:

  1. Create a new AcronymsController.
  2. Register the new type with the application to ensure the controller’s routes get registered.

Build and run the application, then create a new request in RESTed. Configure the request as follows:

Send the request and you’ll get the existing acronyms in your database:

Route groups

All of the REST routes created for acronyms in the previous chapters use the same initial path, e.g.:

app.post("api", "acronyms") { 
  req -> EventLoopFuture<Acronym> in
  let acronym = try req.content.decode(Acronym.self)
  return acronym.save(on: req.db).map { acronym }
}

If you need to change the /api/acronyms/ path, you have to change the path in multiple locations. If you add a new route, you have to remember to add both parts of the path. Vapor provides route groups to simplify this. Open AcronymsController.swift and create a route group at the beginning of boot(routes:):

let acronymsRoutes = routes.grouped("api", "acronyms")

This creates a new route group for the path /api/acronyms. Next, replace:

routes.get("api", "acronyms", use: getAllHandler)

with the following:

acronymsRoutes.get(use: getAllHandler)

This works as it did before but greatly simplifies the code, making it easier to maintain.

Next, open routes.swift and remove the remaining acronym route handlers:

  • app.post("api", "acronyms")
  • app.get("api", "acronyms", ":acronymID")
  • app.put("api", "acronyms", ":acronymID")
  • app.delete("api", "acronyms", ":acronymID")
  • app.get("api", "acronyms", "search")
  • app.get("api", "acronyms", "first")
  • app.get("api", "acronyms", "sorted")

Next, remove any other routes from the template. You should only have the AcronymsController registration left in routes(_:). Next, open AcronymsController.swift and recreate the handlers by adding each of the following after boot(routes:)

func createHandler(_ req: Request) throws 
    -> EventLoopFuture<Acronym> {
  let acronym = try req.content.decode(Acronym.self)
  return acronym.save(on: req.db).map { acronym }
}

func getHandler(_ req: Request) 
    -> EventLoopFuture<Acronym> {
  Acronym.find(req.parameters.get("acronymID"), on: req.db)
    .unwrap(or: Abort(.notFound))
}

func updateHandler(_ req: Request) throws 
    -> EventLoopFuture<Acronym> {
  let updatedAcronym = try req.content.decode(Acronym.self)
  return Acronym.find(
    req.parameters.get("acronymID"),
    on: req.db)
    .unwrap(or: Abort(.notFound)).flatMap { acronym in
      acronym.short = updatedAcronym.short
      acronym.long = updatedAcronym.long
      return acronym.save(on: req.db).map {
        acronym
      }
    }
}

func deleteHandler(_ req: Request) 
    -> EventLoopFuture<HTTPStatus> {
  Acronym.find(req.parameters.get("acronymID"), on: req.db)
    .unwrap(or: Abort(.notFound))
    .flatMap { acronym in
      acronym.delete(on: req.db)
        .transform(to: .noContent)
    }
}

func searchHandler(_ req: Request) throws 
    -> EventLoopFuture<[Acronym]> {
  guard let searchTerm = req
    .query[String.self, at: "term"] else {
      throw Abort(.badRequest)
  }
  return Acronym.query(on: req.db).group(.or) { or in
    or.filter(\.$short == searchTerm)
    or.filter(\.$long == searchTerm)
  }.all()
}

func getFirstHandler(_ req: Request) 
    -> EventLoopFuture<Acronym> {
  return Acronym.query(on: req.db)
    .first()
    .unwrap(or: Abort(.notFound))
}

func sortedHandler(_ req: Request) 
    -> EventLoopFuture<[Acronym]> {
  return Acronym.query(on: req.db)
    .sort(\.$short, .ascending).all()
}

Each of these handlers is identical the ones you created in Chapter 7. If you need a reminder of what they do, that’s the place to look!

Finally, register these route handlers using the route group. Add the following to the bottom of boot(routes:):

// 1
acronymsRoutes.post(use: createHandler)
// 2
acronymsRoutes.get(":acronymID", use: getHandler)
// 3
acronymsRoutes.put(":acronymID", use: updateHandler)
// 4
acronymsRoutes.delete(":acronymID", use: deleteHandler)
// 5
acronymsRoutes.get("search", use: searchHandler)
// 6
acronymsRoutes.get("first", use: getFirstHandler)
// 7
acronymsRoutes.get("sorted", use: sortedHandler)

Here’s what this does:

  1. Register createHandler(_:) to process POST requests to /api/acronyms.
  2. Register getHandler(_:) to process GET requests to /api/acronyms/<ACRONYM ID>.
  3. Register updateHandler(_:) to process PUT requests to /api/acronyms/<ACRONYM ID>.
  4. Register deleteHandler(_:) to process DELETE requests to /api/acronyms/<ACRONYM ID>.
  5. Register searchHandler(_:) to process GET requests to /api/acronyms/search.
  6. Register getFirstHandler(_:) to process GET requests to /api/acronyms/first.
  7. Register sortedHandler(_:) to process GET requests to /api/acronyms/sorted.

Build and run the application, then create a new request in RESTed. Configure the request as follows:

Send the request and you’ll see a previously created acronym using the new controller:

Where to go from here?

This chapter introduced controllers as a way of better organizing code. They help split out route handlers into separate areas on responsibility. This allows applications to grow in a maintainable way. The next chapters look at how to bring together different models with relationships in Fluent.

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.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2021 Razeware LLC