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

16. Making a Simple Web App, Part 1
Written by Tim Condon

In the previous chapters, you learned how to display data in a website and how to make the pages look nice with Bootstrap. In this chapter, you’ll learn how to create different models and how to edit acronyms.

Categories

You’ve created pages for viewing acronyms and users. Now it’s time to create similar pages for categories. Open WebsiteController.swift. At the bottom of the file, add a context for the “All Categories” page:

struct AllCategoriesContext: Encodable {
  // 1
  let title = "All Categories"
  // 2
  let categories: [Category]
}

Here’s what this does:

  1. Define the page’s title for the template.
  2. Define an array of categories to display in the page.

Next, add the following under allUsersHandler(_:) to create a new route handler for the “All Categories” page:

func allCategoriesHandler(_ req: Request) 
  -> EventLoopFuture<View> {
  // 1
  Category.query(on: req.db).all().flatMap { categories in
    // 2
    let context = AllCategoriesContext(categories: categories)
    // 3
    return req.view.render("allCategories", context)
  }
}

Here’s what this route handler does:

  1. Get all the categories from the database like before.
  2. Create an AllCategoriesContext. Notice that the context includes the query result directly, since Leaf can handle futures.
  3. Render the allCategories.leaf template with the provided context.

Create a new file in Resources/Views called allCategories.leaf for the “All Categories” page. Open the new file and add the following:

#extend("base"):
  <!-- 1 -->
  #export("content"):
    <h1>All Categories</h1>
    
    <!-- 2 -->
    #if(count(categories) > 0):
      <table class="table table-bordered table-hover">
        <thead class="thead-light">
          <tr>
            <th>Name</th>
          </tr>
        </thead>
        <tbody>
          <!-- 3 -->
          #for(category in categories):
            <tr>
              <td>
                <a href="/categories/#(category.id)">
                  #(category.name)
                </a>
              </td>
            </tr>
          #endfor
        </tbody>
      </table>
    #else:
      <h2>There aren’t any categories yet!</h2>
    #endif
  #endexport
#endextend

This template is like the table for all acronyms, but the important points are:

  1. Set the content variable for use by base.leaf.
  2. Check if any categories exist.
  3. Loop through each category and add a row to the table with the name, linking to a category page.

Now, you need a way to display all of the acronyms in a category. Open, WebsiteController.swift and add the following context at the bottom of the file for the new category page:

struct CategoryContext: Encodable {
  // 1
  let title: String
  // 2
  let category: Category
  // 3
  let acronyms: [Acronym]
}

Here’s what the context contains:

  1. A title for the page; you’ll set this as the category name.
  2. The category for the page.
  3. The category’s acronyms.

Next, add the following under allCategoriesHandler(_:) to create a route handler for the page:

func categoryHandler(_ req: Request) 
  -> EventLoopFuture<View> {
  // 1
  Category.find(req.parameters.get("categoryID"), on: req.db)
    .unwrap(or: Abort(.notFound)).flatMap { category in
      // 2
      category.$acronyms.get(on: req.db).flatMap { acronyms in
        // 3
        let context = CategoryContext(
          title: category.name,
          category: category,
          acronyms: acronyms)
        // 4
        return req.view.render("category", context)
      }
  }
}

Here’s what the route handler does:

  1. Get the category from the request’s parameters and unwrap the returned future.
  2. Perform a query get all the acronyms for the category using Fluent’s helpers.
  3. Create a context for the page.
  4. Return a rendered view using the category.leaf template.

Create the new template file, category.leaf, in Resources/Views. Open the new file and add the following:

#extend("base"):
  #export("content"):
    <h1>#(category.name)</h1>
    
    #extend("acronymsTable")
  #endexport
#endextend

This is almost the same as the user’s page just with the category name for the title. Notice that you’re using the acronymsTable.leaf template to display the table to acronyms. This avoids duplicating yet another table and, again, shows the power of templates. Open base.leaf and add the following after the link to the all users page:

<li class="nav-item 
 #if(title == "All Categories"): active #endif">
  <a href="/categories" class="nav-link">All Categories</a>
</li>

This adds a new link to the navigation on the site for the all categories page. Finally, open WebsiteController.swift and, at the end of boot(routes:), add the following to register the new routes:

// 1
routes.get("categories", use: allCategoriesHandler)
// 2
routes.get("categories", ":categoryID", use: categoryHandler)

Here’s what this does:

  1. Register a route at /categories that accepts GET requests and calls allCategoriesHandler(_:).
  2. Register a route at /categories/<CATEGORY ID> that accepts GET requests and calls categoryHandler(_:).

Build and run, then go to http://localhost:8080/ in your browser. Click the new All Categories link in the menu and you’ll go to the new “All Categories” page:

Click a category and you’ll see the category information page with all the acronyms for that category:

Create acronyms

To create acronyms in a web application, you must actually implement two routes. You handle a GET request to display the form to fill in. Then, you handle a POST request to accept the data the form sends.

The page to create an acronym needs a list of all the users to permit selecting which user owns the acronym. Create a context at the bottom of WebsiteController.swift to represent this:

struct CreateAcronymContext: Encodable {
  let title = "Create An Acronym"
  let users: [User]
}

Next, create a route handler to present the “Create An Acronym” page under categoryHandler(_:):

func createAcronymHandler(_ req: Request) 
  -> EventLoopFuture<View> {
  // 1
  User.query(on: req.db).all().flatMap { users in
    // 2
    let context = CreateAcronymContext(users: users)
    // 3
    return req.view.render("createAcronym", context)
  }
}

Here’s what this does:

  1. Get all the users from the database.
  2. Create a context for the template.
  3. Render the page using the createAcronym.leaf template.

Next, add the following below createAcronymHandler(_:) to create a route handler for the POST request:

// 1
func createAcronymPostHandler(_ req: Request) throws 
  -> EventLoopFuture<Response> {
  // 2
  let data = try req.content.decode(CreateAcronymData.self)
  let acronym = Acronym(
    short: data.short, 
    long: data.long, 
    userID: data.userID)
  // 3
  return acronym.save(on: req.db).flatMapThrowing {
      // 4
      guard let id = acronym.id else {
        throw Abort(.internalServerError)
      }
      // 5
      return req.redirect(to: "/acronyms/\(id)")
  }
}

Here’s what this does:

  1. Declare a route handler that returns EventLoopFuture<Response>.
  2. Decode the data from the request and use it to create an acronym. You do the same thing in AcronymsController.
  3. Save the acronym and resolve the future. Note the use of flatMapThrowing(_:) here, since the closure doesn’t return a future but can throw.
  4. Ensure that the ID is set, otherwise throw a 500 Internal Server Error.
  5. Redirect to the page for the newly created acronym.

Next, to register these routes, add the following to the bottom of boot(routes:):

// 1
routes.get("acronyms", "create", use: createAcronymHandler)
// 2
routes.post("acronyms", "create", use: createAcronymPostHandler)

Here’s what the code does:

  1. Register a route at /acronyms/create that accepts GET requests and calls createAcronymHandler(_:).
  2. Register a route at /acronyms/create that accepts POST requests and calls createAcronymPostHandler(_:).

You now need a template to display the create acronym form. Create a new file in Resources/Views called createAcronym.leaf. Open the file and add the following:

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

    <!-- 2 -->
    <form method="post">
      <!-- 3 -->
      <div class="form-group">
        <label for="short">Acronym</label>
        <input type="text" name="short" class="form-control"
        id="short"/>
      </div>

      <!-- 4 -->
      <div class="form-group">
        <label for="long">Meaning</label>
        <input type="text" name="long" class="form-control"
        id="long"/>
      </div>

      <div class="form-group">
        <label for="userID">User</label>
        <!-- 5 -->
        <select name="userID" class="form-control" id="userID">
          <!-- 6 -->
          #for(user in users):
            <option value="#(user.id)">
              #(user.name)
            </option>
          #endfor
        </select>
      </div>

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

Here’s what the template does:

  1. Extend base.leaf and define the content variable required.
  2. Create an HTML form. Set the method to POST. This means the browser sends the data to the same URL using a POST request when a user submits the form.
  3. Create a group for the acronym’s short value. Use HTML’s <input> element to allow a user to insert text. The name property tells the browser what the key for this input should be when sending the data in the request.
  4. Create a group for the acronym’s long value using HTML’s <input> element.
  5. Create a group for the acronym’s user. Use HTML’s <select> element to display a drop-down menu of the different users.
  6. Use Leaf’s #for() loop to iterate through the provided users and add each as an option on the <select>.
  7. Create a submit button the user can click to send the form to your web app.

Finally, add a link to the new page in base.leaf just before the </ul> tag:

<!-- 1 -->
<li class="nav-item 
 #if(title == "Create An Acronym"): active #endif">
  <!-- 2 -->
  <a href="/acronyms/create" class="nav-link">
    Create An Acronym
  </a>
</li>

Here’s what the code does:

  1. Add a new navigation item to the nav bar. If you’re on the “Create An Acronym” page, mark the item active.
  2. Add a link to the create page.

Build and run, then open your browser. Navigate to http://localhost:8080 and you’ll see a new option, “Create An Acronym”, in the navigation bar. Click the link to go to the new page. Fill in the form and click Submit.

The app redirects you to the new acronym’s page:

Editing acronyms

You now know how to create acronyms through the website. But what about editing an acronym? Thanks to Leaf, you can reuse many of the same components to allow users to edit acronyms. Open WebsiteController.swift.

At the end of the file, add the following context for editing an acronym:

struct EditAcronymContext: Encodable {
  // 1
  let title = "Edit Acronym"
  // 2
  let acronym: Acronym
  // 3
  let users: [User]
  // 4
  let editing = true
}

Here’s what the context contains:

  1. The title for the page: “Edit Acronym”.
  2. The acronym to edit.
  3. An array of users to display in the form.
  4. A flag to tell the template that the page is for editing an acronym.

Next, add the following route handler below createAcronymPostHandler(_:) to show the edit acronym form:

func editAcronymHandler(_ req: Request) 
  -> EventLoopFuture<View> {
  // 1
  let acronymFuture = Acronym
    .find(req.parameters.get("acronymID"), on: req.db)
    .unwrap(or: Abort(.notFound))
  // 2
  let userQuery = User.query(on: req.db).all()
  // 3
  return acronymFuture.and(userQuery)
    .flatMap { acronym, users in
      // 4
      let context = EditAcronymContext(
        acronym: acronym, 
        users: users)
      // 5
      return req.view.render("createAcronym", context)
  }
}

Here’s what this route does:

  1. Create a future to get the acronym to edit from the request’s parameters.
  2. Create a future to get all the users from the DB.
  3. Use .and(_:) to chain the futures together and flatMap(_:) to wait for both futures to complete.
  4. Create a context to edit the acronym, passing in all the users.
  5. Render the page using the createAcronym.leaf template, the same template used for the create page.

Next, add the following route handler for the POST request from the edit acronym page below editAcronymHandler(_:):

func editAcronymPostHandler(_ req: Request) throws 
  -> EventLoopFuture<Response> {
  // 1
  let updateData = 
    try req.content.decode(CreateAcronymData.self)
  // 2
  return Acronym
    .find(req.parameters.get("acronymID"), on: req.db)
    .unwrap(or: Abort(.notFound)).flatMap { acronym in
      // 3
      acronym.short = updateData.short
      acronym.long = updateData.long
      acronym.$user.id = updateData.userID
      // 4
      guard let id = acronym.id else {
        let error = Abort(.internalServerError)
        return req.eventLoop.future(error: error)
      }
      // 5
      let redirect = req.redirect(to: "/acronyms/\(id)")
      return acronym.save(on: req.db).transform(to: redirect)
  }
}

Here’s what the route does:

  1. Decode the request body to CreateAcronymData.
  2. Get the acronym to edit from the request’s parameters and resolve the future.
  3. Update the acronym with the new data.
  4. Ensure the ID is set, otherwise return a failed future with a 500 Internal Server Error.
  5. Save the updated acronym and transform the result to redirect to the updated acronym’s page.

Next, add the following to register the two new routes at the bottom of boot(routes:):

routes.get(
  "acronyms", ":acronymID", "edit",
   use: editAcronymHandler)
routes.post(
  "acronyms", ":acronymID", "edit", 
  use: editAcronymPostHandler)

This registers a route at /acronyms/<ACRONYM ID>/edit to accept GET requests that calls editAcronymHandler(_:). It also registers a route to handle POST requests to the same URL that calls editAcronymPostHandler(_:).

Open createAcronym.leaf and change the template to accommodate editing an acronym. First, replace the input for the acronym short to accommodate editing:

<input type="text" name="short" class="form-control"
 id="short" #if(editing): value="#(acronym.short)" #endif/>

If the editing flag is set, this sets the value attribute of the <input> to the acronym’s short property. This is how you pre-fill the form for editing. Do the same for the acronym’s long input:

<input type="text" name="long" class="form-control"
 id="long" #if(editing): value="#(acronym.long)" #endif/>

Replace the users’ <select> option for editing:

<option value="#(user.id)"
 #if(editing): #if(acronym.user.id == user.id): 
   selected #endif #endif>
  #(user.name)
</option>

This sets the <option>’s selected property if the user’s ID matches the acronym’s userID. This makes that option in the drop-down menu appear as the selected one. Next, replace the button for submitting the form:

<button type="submit" class="btn btn-primary">
  #if(editing): Update #else: Submit #endif
</button>

This uses Leaf’s #if()/else tags to set the text of the button to “Update” or “Submit” depending on the page’s mode.

Finally, open acronym.leaf and add a button to edit that acronym at the bottom of #export("content")::

<a class="btn btn-primary" href="/acronyms/#(acronym.id)/edit"
 role="button">Edit</a>

This creates an HTML link to /acronyms/<ACRONYM ID>/edit and uses Bootstrap to style the link as a button. Save the files and in Xcode, build and run the app. Open http://localhost:8080/ in your browser.

Open an acronym page and there’s now an Edit button at the bottom:

Click Edit to go to the edit acronym page with all the information pre-populated. The title and button are also different:

Change the acronym and click Update. The app redirects you to the acronym’s page and you’ll see the updated information.

Deleting acronyms

Unlike creating and editing acronyms, deleting an acronym only requires a single route. However, with web browsers there’s no simple way to send a DELETE request.

Browsers can only send GET requests to request a page and POST requests to send data with forms.

Note: It’s possible to send a DELETE request with JavaScript, but that’s outside the scope of this chapter.

To work around this, you’ll send a POST request to a delete route.

Open, WebsiteController.swift and add the following route handler below editAcronymPostHandler(_:) to delete an acronym:

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

This route extracts the acronym from the request’s parameter, unwraps the future and calls delete(on:) on the acronym. The route then transforms the result to redirect the page to the home screen. Register the route at the bottom of boot(routes:):

routes.post(
  "acronyms", ":acronymID", "delete", 
  use: deleteAcronymHandler)

This registers a route at /acronyms/<ACRONYM ID>/delete to accept POST requests and call deleteAcronymHandler(_:). Build and run. Open acronym.leaf and replace the edit button with the following:

<!-- 1 -->
<form method="post" action="/acronyms/#(acronym.id)/delete">
  <!-- 2 -->
  <a class="btn btn-primary" href="/acronyms/#(acronym.id)/edit"
   role="button">Edit</a>&nbsp;
  <!-- 3 -->
  <input class="btn btn-danger" type="submit" value="Delete" />
</form>

Here’s what the new code does:

  1. Declare a form that sends a POST request. Set the action property to /acronyms/<ACRONYM ID>/delete. It’s good practice to use a POST request for actions that modify the database, such as create or delete. This enables you to protect them with CSRF (Cross Site Request Forgery) tokens in the future, for example.
  2. Incorporate the edit button that already exists on the page. This allows Bootstrap to align them. Use Bootstrap’s button styling so the buttons look the same.
  3. Create a submit button for the delete form.

Save the file, then open http://localhost:8080/ in the browser. Open an acronym page and you’ll see the delete button:

Click Delete to delete the acronym. The app redirects you to the homepage and the deleted acronym is no longer shown.

Where to go from here?

In this chapter, you learned how to display your categories and how to create, edit and delete acronyms. You still need to complete your support for categories, allowing your users to put acronyms into categories and remove them. You’ll learn how to do that in the next chapter!

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.