0% found this document useful (0 votes)
94 views325 pages

Practical Server Side Swift 1.5.0

Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
94 views325 pages

Practical Server Side Swift 1.5.0

Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 325

PRACTICAL

SERVER
SIDE
SWIFT
THIRD EDITION
BY TIBOR BÖDECS
PRACTICAL

SERVER
SIDE
SWIFT
BY TIBOR BÖDECS

THIRD EDITION
VERSION 1.5.0
PUBLISHED BY TIBOR BÖDECS
3RD OF APRIL 2023
PRACTICAL SERVER SIDE SWIFT

Copyright © 2020-2023 Tibor Bödecs.

All rights reserved.

Swift, the Swift logo, Swift Playgrounds, Xcode, iOS, macOS, watchOS, tvOS, and Mac are
trademarks of Apple Inc., registered in the U.S. and other countries.

Docker and DockerHub is a registered trademark of Docker Inc.

Amazon Web Services, AWS, Simple Storage Service, Elastic Compute Cloud, Elastic Cloud
Repository, Elastic Container Service, Elastic Load Balancer, Relational Database Service,
Fargate and Route 53 are trademarks of Amazon Web Services, Inc.
ABOUT THE AUTHOR

Tibor Bödecs is an enthusiastic software developer with more than a decade of experience
in the IT industry. In his past, Tibor was the technology leader at one of the biggest mobile
development-focused companies in Hungary, then he was a freelancer / tech consultant
working with clients from all around the world.

He is a self-taught programmer with a true passion for Swift from the very beginning. He has
a good ability to work with di erent languages, technologies, and extensive experience in
product management.

Currently he is the CEO of Binary Birds Ltd., a small company focusing mostly on server side
Swift development and consulting. Tibor has a personal blog called The.Swift.Dev. where he
regularly writes about the Swift programming language.

Tibor Bödecs (Tib)


twitter.com/tiborbodecs
[email protected]

binarybirds.com
theswiftdev.com
ff
ABOUT THE REVIEWER

Over the past 10 years, I've honed my skills as an iOS developer, moving from an employee
in a small company to a freelancer and now the co-founder of Binary Birds. During this time,
I've had the opportunity to work on a wide range of projects across various industries,
including automotive, banking, telecommunication, e-commerce, and sports. Although I've
always enjoyed working on iOS apps, I recently felt the need for a new challenge. This led
me to explore server-side Swift, which felt like a logical next step due to my early adoption of
the language.

Fortunately, the stars aligned when I had the chance to work with Tibor and contribute to the
next version of his book, Practical Server Side Swift. As someone who is both new to the
eld but experienced with Swift, I was able to review and update the book, making it more
relevant and up-to-date for future readers. It was a privilege to lend my skills to such a
valuable resource for those looking to learn about building a blog engine using the Vapor
framework.

Overall, my experience and passion for iOS development led me to explore new challenges,
and working with Tibor on his book was the perfect opportunity for me to expand my skill set
and contribute to the community of developers who want to learn about server-side Swift.

Ferenc Viasz-Kádi (Viasz)


https://twitter.com/viaszkadi

binarybirds.com
fi
ABOUT THE EDITOR

Michael J. Welch, Ph.D. (Mike) is a grumpy old programmer who began his career working on
a System 360/65 mainframe computer with PL/1 in 1967. He has more than ve decades of
programming experience using Fortran, PL/1, Assembly, Pascal, C, C++, Java, Visual Basic,
Visual Studio, Ruby, Javascript, HTML, and other languages you've never heard of. He's
worked on mainframe, mini, and microcomputers, most of which, if they haven't already been
scrapped, are only in museums. Today, he works on macOS.

He worked for a decade at North American Aviation's Microelectronics Division (now


Rockwell International). He also created simulation, design, layout, and mask cutting
software for Hycom, a division of Sharp of Japan, and the developers of the rst high-speed
modem (9600 baud: that was high back then). He also worked for many smaller companies
as a programmer, software development team leader, manager, CTO, and CIO.

Mike has a Ph.D. in Business Administration and is currently employed as the Chief
Operating O cer of a 13 physician Medical Corporation in Southern California. Besides
operating the corporation, Mike also has a hand in the development of software for
specialized (non-commercial) medical billing in Ruby on a Debian Linux server.

Another self-taught programmer like Tibor Bödecs (Tib), Mike's next challenge was to learn
Swift and Swift server-side application development, and that's how he met Tib: Tib's new
book Practical Server Side Swift (this book) was just what he needed; Mike volunteered and
became a collaborator and book editor.

Michael J. Welch, Ph.D. (Mike)


[email protected]
ffi
fi
fi
Thank you for purchasing this book.

Special thanks to tdotclare for creating the Tau template engine.

Special thanks to Michael Redig for the feedbacks and review.


TABLE OF CONTENTS

Preface 1

Chapter 1: Introduction 6

The Swift programming language 6

The Swift ecosystem 7

Swift on the server 8

The Vapor framework — a Swift Web Server 8

Summary 11

Chapter 2: Getting started with Vapor 12

Installing Swift 12

My rst Vapor app 15

The Vapor toolbox 19

Setting up custom Working directories 20

Anatomy of a Vapor project 22

Summary 24

Chapter 3: Getting started with SwiftHtml 25

Rendering templates using SwiftHtml 25

Templates and contexts 30

Summary 52

Chapter 4: Getting started with Fluent 53

Working with models 54

Database migrations 57

Querying models and Data Transfer Objects (DTOs) 65

Summary 69

Chapter 5: Sessions and user authentication 70

The user module 70

Signing in 73
fi
Authenticators 77

Summary 83

Chapter 6: Abstract forms and form elds 84

Reusable form elds 84

Form components 89

Refactoring the user login 92

Summary 96

Chapter 7: Form events and async validation 97

Form event handlers 97

Async form validation 100

Summary 108

Chapter 8: Advanced form elds 109

Hidden eld 109

Textarea eld 110

Select eld 112

File uploads & image eld 115

Summary 123

Chapter 9: Content Management System 124

The admin module 124

List 131

Detail 135

Create 140

Update 144

Delete 146

Summary 148

Chapter 10 Building a generic admin interface 149

Generic table template system 149

Generic model and list controllers 154

Generic detail controller 162


fi
fi
fi
fi
fi
fi
fi
Generic editor controllers 168

Generic delete controller 179

Summary 184

Chapter 11: A basic a REST API layer 185

It's all about data models and CRUD 185

List API endpoint 186

Detail API endpoint 188

Create API endpoint 189

Update API endpoint 191

Patch API endpoint 193

Delete API endpoint 194

Summary 195

Chapter 12: Building a generic REST API 197

List 197

Detail 199

Create 201

Update 203

Patch 205

Delete 207

Admin and API controller 210

Names and path keys 215

A better routing system 219

Summary 230

Chapter 13: API protection and validation 231

User authentication using bearer tokens 231

Protecting API endpoints 235

API validation 237

API lifecycle methods 251

Summary 260
Chapter 14: System Under Testing 261

Getting started with unit testing 261

Testing with user authentication 263

Declarative unit testing 273

Summary 282

Chapter 15: Event driven hook functions 283

Generic hook functions 283

Using the hook system 287

Enhanced routing system 289

The asynchronous hook system 294

Summary 300

Chapter 16: Shared API library packages 301

Creating a shared API library 301

Using the shared API library 309

Summary 312

Epilogue 313
PREFACE

Vapor is the most popular server-side Swift framework. With this book, I'll teach you how to
build real-world apps with a modular architecture in mind.

In the upcoming chapters, you'll be able to learn about the fundamentals of backend apps
using asynchronous APIs and the Vapor ecosystem using the Swift language.

Swift is one of the fastest-growing programming languages in the industry. A few years ago
Chris Lattner was interviewed at the WWDC Swift panel and told us about his vision:

"MY GOAL FOR SWIFT HAS ALWAYS BEEN, AND STILL IS,
TOTAL WORLD DOMINATION. IT’S A MODEST GOAL"
— Chris Lattner

"So how are we going to achieve that?" he asks. "It's the same process as writing a big app:
you take the goal and decompose it into sub-problems and then solve the individual
problems. And the way you get to world domination with a language is you have to have a
killer app rst."

In this book, we're going to follow the same approach: to write a killer server-side Swift app
using smaller modules. We're going to create a blog engine from scratch.

Go on the journey with me and learn how to make backend apps using these amazing tools!

"THE SECRETS OF THIS EARTH ARE NOT FOR ALL MEN TO SEE,
BUT ONLY FOR THOSE WHO WILL SEEK THEM."
— Ayn Rand, Anthem, pg. 52

WHO'S THIS BOOK FOR?


This book is for anyone interested in building backend apps using the Swift programming
language and the Vapor framework. This book is written for people who're already familiar
with the Swift programming language. Anyone with basic Swift knowledge should be able to
follow the chapters. iOS developers with minor server-side experience can extend their skills
to become full-stack developers by reading this book.

1
fi
TIPS ON READING
To get the most out of this book, you should follow the chapters in order just as they are.
Move forward with each chapter at your own pace. Always try to understand every single
concept before you proceed to the next chapter. If you need to, just go back and read a few
sections or even chapters again. There are some tasks that you have to do on your own
while reading the book; they can help you by practicing what you've learned.

BOOK OVERVIEW
Chapter 1: Introduction

We start with an introduction to the Server Side Swift world, explaining the evolution of Swift
as a universal programming language. We'll talk about both the strengths and weaknesses
of the language and discuss why Swift is a good choice to build backend applications. We'll
explore the Swift ecosystem and the open-source movement that made it possible to create
the necessary tools on Linux to turn Swift into a server-side language. You'll get introduced
to Vapor, the most popular web application framework, that we're going to use in this book.

Chapter 2: Getting started with Vapor

Next, we go over detailed instructions about how to install all the required components to
build server-side Swift applications both on Linux and macOS. You'll meet some command-
line tools that can help your everyday life as a backend developer and we'll create our very
rst Vapor project using the Swift Package Manager. We'll also set up the Vapor toolbox, a
handy little tool that can help you bootstrap projects based on a template. In the very last
section, we'll brie y take a look at the architecture of a Vapor application.

Chapter 3: Getting started with SwiftHtml

Then we're going to build our rst website using the SwiftHtml library, and we're going to
generate HTML code through Swift by creating template les using a Domain Speci c
Language (DSL). You'll learn about how to connect SwiftHtml with Vapor and how to render
HTML by using context variables to provide additional template data. You'll learn about the
syntax of SwiftHtml, how to iterate through objects, how to check optional variables, and
how to extend a base template to provide a reusable framework for our website, and nally,
we'll build a simple blog layout with a post, list, and detail pages.

Chapter 4: Getting started with Fluent

Here you'll learn about the Fluent ORM framework and the advantages of using such a tool
instead of writing raw database queries. We'll set up Fluent, powered by the SQLite driver,
and model our database elds using property wrappers in Swift. We're going to provide a
seed for our database, get familiar with migration scripts, and make some changes on the
website so it can query blog posts from the local database and render them using view
templates.

2
fi
fl
fi
fi
fi
fi
fi
Chapter 5: Sessions and user authentication

We're going to focus on building a session-based web authentication layer that users will be
able to use to sign in using a form, and with which already logged in users will be
authenticated with the help of a session cookie and persistent session storage using Fluent.
In the second half of this chapter, I'll show you how to create a custom authenticator
middleware that'll allow you to authenticate users based on sessions or credentials.

Chapter 6: Abstract forms and form elds

Building forms is all about creating an abstract form builder that we can use to generate the
HTML forms. We're going to de ne reusable form elds with corresponding context objects
using a model-view-like architecture. This will allow us to compose all kinds of input forms by
reusing the generic elds. In the second half of the chapter, we're going to talk about
processing user input and loading and persisting data using a protocol-oriented solution.
Finally, we're going to rebuild our already existing user login form by using those
components.

Chapter 7: Form events and async validation

Next, we're going to work a little bit on our form components. We're going to implement
more event handler methods and you're going to learn the preferred way of calling them to
build a proper create or update work ow ow. The second half of the chapter is all about
building an asynchronous validation mechanism for the abstract forms. We're going to build
several form eld validators and nally, you'll see how to work with these validators and
display user errors to improve the overall experience.

Chapter 8: Advanced form elds

This chapter is all about building new form elds that we're going to use later on. You'll learn
how to build custom form elds based on the abstract form eld class, so by the end of this
chapter, you should be able to create even more form elds to t your needs. We're also
going to introduce a brand new Swift package called Liquid that's a le storage driver made
for Vapor. By using this library, we're going to be able to create a form eld for uploading
images.

Chapter 9: Content Management System

Here you'll learn how to build a basic content management system with an admin interface.
We're going to create a standalone module for the admin views that'll be completely
separated from the web frontend. The CMS will support list, detail, create, update and delete
functionality. Models are going to be persisted to the database and we'll secure the admin
endpoints by using a new built-in middleware.

Chapter 10: Building a generic admin interface

This chapter is about turning our basic CMS into a generic solution. By leveraging the power
of Swift protocols, we're going to be able to come up with several base controllers that can
be used to manage database models through the admin interface. This methodology allows
us to easily de ne a list, create, update and delete controllers. By the end of this chapter,
we're going to have a completely working admin solution for the blog module.

3
fi
fi
fi
fi
fi
fi
fi
fl
fi
fl
fi
fi
fi
fi
fi
fi
fi
Chapter 11: A basic REST API layer

Next, you'll learn about building a standard JSON-based API service. In the rst section, we'll
discuss how to design a REST API then we'll build the CRUD endpoints for the category
controller. We'll also talk a bit about the HTTP layer and learn how to use the cURL
command-line utility to test the endpoints. You'll discover why it's a better practice to use
standalone data transfer objects (DTOs) rather than expose database models to the public.

Chapter 12: Building a generic REST API

This chapter contains useful materials about how to turn our REST API layer into a reusable
generic solution. We're going to de ne common protocols that'll allow us to share some of
the logic between the admin and API controllers. The rst part's going to be all about the
controller updates, but later on in this chapter, we're also going to improve the routing
mechanism by introducing new setup methods for the route handlers.

Chapter 13: API protection and validation

Here you'll learn about making the backend service more secure by introducing better API
protection and validation methods. The rst part is about user authentication using bearer
tokens. We're going to create a new token-based authenticator and guard the API endpoints
against unauthenticated requests. The second part is going to be all about data validation
using the async validator logic that we created a few chapters before. In the very last section
of this chapter, we're going to introduce some additional lifecycle methods for the
controllers.

Chapter 14: System under testing

For testing, you'll learn the brand new XCTVapor framework. First, we'll set up the test
environment, write some basic unit tests for our application, and then run them. Next, we're
going to dig a little bit deeper into the XCTVapor framework so you can see how to write
more complex tests. In the last part, you'll meet a super lightweight and clean testing tool.
The Spec library will allow us to write declarative speci cations for our test cases.

Chapter 15: Event driven hook functions

After that, we're going to eliminate the dependencies between the modules by introducing a
brand new event-driven architecture (EDA). By using hook functions, we're going to be able
to build connections without the need of importing the interface of one module into another.
The EDA design pattern allows us to create loosely coupled software components and
services without forming an actual dependency between the participants.

Chapter 16: Shared API library packages

Last but not least, this chapter teaches you how to separate the data transfer object (DTO)
layer into a standalone Swift package product: this way you'll be able to share server-side
Swift code with client apps. In the rst part of the chapter, I'm going to show you how to set
up the project then we're going to add access control modi ers to allow other modules to
see our DTOs. The second half of the chapter is going to give you some really basic
examples of how to perform HTTP requests using the modern Swift concurrency APIs.

4
fi
fi
fi
fi
fi
fi
fi
CODE SAMPLES
You can nd the related sample codes in the following GitHub repository.

Please make sure that you're using the v1.5.0 release.

GET IN TOUCH
Feel free to send me your thoughts so I can improve both the samples and the book.

General feedback

If you nd a mistake in this book, just tell me about it and I'll x it as soon as possible.

Piracy report

If you come across any illegal copies of this book on the Internet, I would be grateful if you
could provide me the link or website name, so I can take the necessary actions.

Reviews

Once you've read this book please consider leaving a review on the following link.

CONTACT DETAILS
Please don't hesitate to contact me using the options below.

- Web: theswiftdev.com

- Email: [email protected]

- Twitter: @tiborbodecs

Thank you very much for your help and support.

5
fi
fi
fi
CHAPTER 1:
INTRODUCTION

This chapter is an introduction to the server-side Swift world, explaining the evolution of
Swift as a universal programming language. We'll talk about both the strengths and
weaknesses of the language and discuss why Swift is a good choice to build backend
applications. We'll explore the Swift ecosystem and the open-source movement that made it
possible to create the necessary tools on Linux to turn Swift into a server-side language.
You'll get introduced to Vapor (the most popular web application framework) that we're
going to use in this book.

THE SWIFT PROGRAMMING LANGUAGE


Swift is a modern, interactive, type-safe programming language with performance in mind.
The development of the language was started by Chris Lattner back in 2010. The very rst
public appearance of the language was in 2014 at the Apple Worldwide Developer
Conference (WWDC) event, and about one year later, it was open-sourced on GitHub. Over
the years, Swift has been the target of much love and hate. In the early years, it was heavily
criticized because of rapid language change, slow compile-time, unstable application binary
interface (ABI), lack of proper developer tools, and much more.

On the other hand, people loved Swift because it's a very fast compiled language; thanks to
the LLVM infrastructure, the compiler can make some smart decisions in the background to
optimize the performance. That's one of the many reasons we can compare Swift to C in
terms of speed. It has great interoperability with languages from the C family. Swift has good
memory management tools and gives you memory safety by default. The language uses
Automatic Reference Counting (ARC), so there's no need for a garbage collector.

Swift is somewhat a mixture of all things good from other languages. In Swift, you can write
an app using an Object-Oriented Programming (OOP) paradigm, but it also has some
features that usually only apply to Functional Programming (FP) languages. Most importantly,
it implements Apple's (relatively) new Protocol Oriented Programming (POP) paradigm, a
revolutionary new approach to solving problems through composition instead of inheritance.
POP is all about creating small reusable components (as interfaces/protocols) and we can
implement more complex features by composing these protocols.

Swift was designed to be a general-purpose language, so you can build apps, servers,
scripts, or even operating systems with it. The syntax of Swift is very similar to the popular
JavaScript language. This means that Swift is as easy to use as a scripting language, without
sacri cing any performance. You can use classes, structs, enums, protocols, functions,
closures, generics, and many more constructs. In the last few years, lots of new features
were added to the language.

Swift is already 9 years old now, so we can safely consider it a mature language. In the past,
the lack of an ABI and module stability were huge pain points for many developers.
Fortunately, the Swift compiler infrastructure improved a lot; tools are also getting better and

6
fi
fi
better; and version 5.7 is both ABI and module stable. Swift performs extremely well: it has a
small memory footprint, the language itself is pretty lightweight, and yet extremely capable
of building amazing things. Swift has quickly become one of the fastest-growing
programming languages.

THE SWIFT ECOSYSTEM


Open-sourcing Swift was a bold move for Apple since they have quite a history of using
closed-source technologies. Thanks to this e ort, Swift can grow faster with the help of the
Swift community. The Swift programming language has an o cial website. It's not just a
simple download page, manual, and language speci cation site, but it contains information
about the compiler, the standard library, the package manager, core libraries, REPL, and a
debugger. It also serves as home to the Server Side Work Group.

Since Swift is open-source, anyone can contribute to the language. To submit changes, rst,
you have to submit a proposal, then your proposal will be reviewed by the core team: this
process is called Swift evolution. The Swift evolution dashboard is the place where you can
track language evolution proposals. It's a public website, so you can submit your ideas using
the Swift evolution repository on GitHub.

Swift is o cially supported on macOS as part of the Xcode developer tool, and it's now
o cially supported on Linux as well. It was just announced that Swift will also be available on
Windows. This means that the language can be used on all the major operating systems. You
can download the latest version from the o cial website.

Swift is very popular, and it's still under active development. The community helped a lot, so
it's clear that Apple made a good decision by open-sourcing the language. The company has
also published lots of components, open-source packages, and tools that are part of the
language infrastructure. Apple also maintains a custom fork of the LLVM project to build the
necessary debugger tools for Swift.

Here are some links that might be helpful to you:

-- The Swift Programming Language - Swift


The LLVM Project - debugger
-- Standard Library Preview - stdlib (evolution) preview
The Foundation Project - Foundation
-- The libdispatch Project - Dispatch (GCD)
The XCTest Project - XCTest
-- Swift Package Manager - SPM
Swift Xcode Playground Support - PlaygroundSupport

These repositories are the fundamental components of the open-source Swift infrastructure.
Apple ships almost everything under the Apache 2.0 license, so if you want to contribute
you should keep this in mind.

7
ffi
ffi
ffi
ff
fi
ffi
fi
SWIFT ON THE SERVER
Open-sourcing the language made it possible to use Swift on Linux. The current
infrastructure allows you to create a fully- edged multi-threaded backend server that can
outperform many other popular solutions.

In the beginning, a big problem was that there was no standard solution for building server-
side applications on Linux. At that time, Swift was quite a young and unstable language, but
it also changed a lot every single year. The Foundation framework on Linux was quite buggy,
so developers struggled with it; sometimes they made custom implementations for low-level
network tasks from the ground up in Swift or created wrappers around existing C libraries.
Honestly? That wasn't an ideal environment to maintain a stable backend application. Those
are the reasons why most of the server-side frameworks slowly faded away.

Later on, the Swift Server Work Group was born to help and coordinate all the server-side
Swift application development e orts by the community. It has a core team, and the
membership is contribution-based. In the past, IBM was a big contributor, but unfortunately,
from the beginning of 2020, they aren't taking part in this e ort. The good news is that the
developer community seems like it's big and active enough to ll this gap.

Apple also realized that the lack of a uni ed low-level async networking framework makes
Swift hard to use on the server. There was a high demand for a capable server-side
networking tool including security and encryption features (for SSL/TLS-based secure
transport) with HTTP and web-socket service support. Apple secretly developed SwiftNIO for
this purpose and it changed everything. Norman Maurer is the person behind Netty, which is
a low-level asynchronous event-driven application framework designed for high
performance (non-blocking IO and scalability) for servers and clients. SwiftNIO is based on
the foundations of Netty but written entirely in Swift for the server.

It seems like both Apple and the Server Side Work Group have some real plans for SwiftNIO
in the future. It's already a foundation block for many open-source projects; it's also gained
support for the HTTP/2 protocol in early 2019 which only made it even more popular in the
community. The SSWG has an incubation process for open-source projects to become
standard, recommended solutions for developing Swift applications on the server. You can
check the currently involved projects on swift.org.

Building a server-side Swift framework was a real struggle in the past, and many of the then
existing frameworks weren't able to continue development due to these extreme conditions.
The Perfect framework became very popular in the early days, but it was slowly abandoned.
Kitura was the second big framework, it was well-supported by IBM, so people had high
hopes. Unfortunately, IBM o cially shut down the project at the end of 2019. There were
some smaller frameworks, Zewo grew quite big over time, but none of them were as
successful as Vapor.

THE VAPOR FRAMEWORK — A SWIFT WEB SERVER


Vapor is a community-driven server-side Swift web framework built on top of SwiftNIO. It's an
open-source project with over 20 thousand stars on GitHub. Vapor has a mature ecosystem
with really well-designed tools, each focusing only on a speci c subset of tasks.

8
ffi
ff
fi
fl
ff
fi
fi
FEATURES
Vapor has

- non-blocking IO that increases performance for servers

- an expressive, protocol-oriented design with a focus on type-safety

- a bulletproof solution for handling routes using a trie-node router

- interactive CLI tools

- parsing and serializing multipart and URL-encoded data

- user authentication

- web-socket support

FLUENT — VAPOR'S OBJECT-RELATIONAL MAPPING (ORM) FRAMEWORK


Fluent

- has support for multiple database drivers such as

- SQLite

- PostgreSQL

- MySQL

- MongoDB

- has a database model abstraction layer

- has database schema migration support

- supports references between models (through relations)

- has a high-level query API written in Swift

- takes advantage of Swift's strong type system

- has transaction support (perform multiple operations)

- replaces raw queries with type-safe operations

SWIFTHTML — SIMPLIFYING THE PROCESS OF WRITING HTML


SwiftHtml — A modern state-of-the-art domain-speci c language (DSL) for Swift. It is initially
created by Tibor Bödecs to replace the Leaf template engine for Vapor applications.

SwiftHtml

- is a Swift library that can be used to generate both static and dynamic HTML

- generates web pages using Swift language statements

9
fi
- is checked by Swift's compiler for errors at compile time

- is type-safe and avoids the most pernicious run-time errors

- is simpler than writing HTML

- automatically generates both opening and closing tags

- you won't be able to misspell a tag or misplace a closing tag

The SwiftHtml package has multiple library products. The core component is called
SwiftSgml, which is an abstract interface for the other SGML-based libraries. The SwiftHtml
product contains most of the necessary HTML tags, and the SwiftSvg library can help you
dynamically build SVG tags. The SwiftSitemap product can generate a sitemap.xml le and
SwiftRss dependency can help you with proper RSS feed generation.

VAPOR SUPPORTS JSON WEB TOKENS (JWT)


JSON Web Tokens are popular nowadays, and Vapor has a new package called JWT that
makes them easy to use. Using the package, you can generate, sign and verify JWT tokens
with just a few lines of code without getting your hands dirty with cryptography.

VAPOR 4 HAS A BRAND NEW TASK QUEUE SYSTEM


Vapor 4 also has a brand new task queue system that'll allow you to schedule job handlers
for long-running tasks in a separate process. The queue handlers can work by using the
Redis in-memory storage driver. Redis is available to use in your Vapor app without the
Queue package.

VAPOR SUPPORTS BACKEND FOR FRONTEND


If your API was created for a large desktop app and you want to create, for example, an iOS
client or a webpage on top of that, you may need your output to ful ll the needs of the new
clients. One way to solve this would be to write a middleware that can sit in between the
original backend and the frontend: this is called a backend-for-frontend (BFF). Vapor is an
ideal candidate for writing BFFs.

You can even share data transfer objects (DTOs) between the frontend clients if they're
written in Swift. Shared Swift packages will help you to eliminate code duplications and API
communication errors.

If you write a BFF, you'll want to design it to have rst-class support for iOS clients. One
speci c issue is sending push noti cation messages to the devices. Fortunately, Vapor has a
built-in solution for APNS — Apple Push Noti cation provider using the APNSwift package.

10
fi
fi
fi
fi
fi
fi
VAPOR 4 HAS MANY OTHER NEW THINGS TO OFFER TOO
- asynchronous HTTP client

- great support for writing command-line scripts

- a brand new service con guration interface

- HTTP2

- streaming

- synchronous content

- back-pressure

- graceful shutdown

and so many more. The community is also building lots of useful components; you should
check the libraries here.

VAPOR IS A COMPLETE SOLUTION


Vapor is written completely in Swift for fast execution and has everything that a backend
developer will need to build server-side applications.

O cial Vapor repositories are hosted on GitHub. There's an o cial documentation for every
major Vapor version that you can always refer to, but sometimes might have to look at the
source code or ask the community on Discord to nd the missing piece of the puzzle. Vapor
people are extremely friendly and helpful.

SUMMARY
In this chapter, we've covered the entire Swift ecosystem and learned about how the Server
Side Work Group helps to improve open-source backend tools for the language. The history
of server-side frameworks was an adventurous journey, but as you can see, if it comes to
building a backend application, Vapor is a great choice. Just like Swift, the Vapor web
framework has changed a lot over time, but now it's a mature tool and has the required
performance, thanks to Swift & SwiftNIO.

The Swift language is Apple's platform for the future, and that means it's here to stay.
Besides Apple's support for Swift, the Vapor community has reached a critical amount of
supporters to keep the project alive for a long time. Vapor's ecosystem is quite large, so
you'll nd the component that you're looking for. In the next chapter, we're going to set up
the necessary tools to build our rst Vapor app.

11
ffi
fi
fi
fi
fi
ffi
CHAPTER 2:
GETTING STARTED WITH VAPOR

This chapter contains detailed instructions about how to install all the required components
to build server-side Swift applications both on Linux and macOS. You'll meet some
command-line tools that can help your everyday life as a backend developer and we'll create
our very rst Vapor project using the Swift Package Manager. We'll also set up the Vapor
toolbox, a handy little tool that can help you bootstrap projects based on a template. In the
very last section, we'll brie y take a look at the architecture of a Vapor application.

INSTALLING SWIFT
The very rst thing you need is a Mac or a PC that can run Swift 5.7. You can nd the
supported operating systems on the o cial swift.org website, but for the sake of simplicity
let's just say that the latest macOS or Ubuntu Linux is a good choice. Vapor 4 requires Swift
5.2, but I'd recommend installing the latest version of the programming language (so in our
case we're going to setup Swift 5.7) because there are many great new features added to
Swift, and you'll bene t from the under the hood security and performance improvements.
Also, we're going to take advantage of the recently introduced async/await feature, so Swift
5.5 is the minimum requirement for our sample projects.

SWIFT ON MACOS
Swift and all of its dependencies are bundled with Xcode on macOS. The latest version of
the developer tool is available as a free download from the Mac App Store. After the
download nishes, you have to open Xcode to complete the installation. After the rst
launch, Xcode will install some additional developer tools on your Mac; you might have to
enter your password to proceed with the installation. When the process nishes, you're
ready to use Swift on macOS. Please note that if you want to use async / await on older
operating systems you'll need Xcode 14.2, otherwise your project won't work. If you can,
please use macOS 12 (Ventura) because async / await works better on the latest operating
system. These instructions should work with macOS 12 (Monterey) also.

SWIFT ON LINUX & AWS


Under Linux I highly recommend installing Swift 5.7 or later. Swift is supported on many Linux
distributions, you should always check your system information rst, then you can download
the latest Swift release from swift.org.

12
fi
fi
fi
fi
fl
ffi
fi
fi
fi
fi
In the instructions below, we'll be using an AWS EC2 instance. The Amazon Linux 2 OS is
the preferred OS on AWS, so the examples below will be using that OS. If you're using
another OS, please refer to the Swift Linux installation instructions for instructions.

Open the download releases webpage in your browser from here. Under Platform, right-
click the Amazon Linux 2 link and choose Copy Link. Download the compressed release le
onto your server using curl or wget (wget is used in the example). This can take a little time:
these releases are usually more than 500 MB.

# wget <the link from the download releases page>

# example:
wget https://download.swift.org/swift-5.7.2-release/amazonlinux2/swift-5.7.2-RELEASE/
swift-5.7.2-RELEASE-amazonlinux2.tar.gz

For instructions on how to use wget to check the signature of the downloaded le, refer
back to the Swift download page.

When the download is complete, you'll get a message something like 2023-03-01 09:27:14
(5.46 MB/s) - swift-5.7.2-RELEASE-amazonlinux2.tar.gz saved [530879551/530879551].

You'll need to know the tar- lename (swift-5.7.2-RELEASE-amazonlinux2.tar.gz in this


example) in the next step. You can also just use ls to get the lename.

But, before we extract the archive, we have to install the required dependencies.

# e.g. Swift dependencies on Amazon Linux 2 (install as root using sudo).


# For other distributions, refer back to the Swift download page.

sudo yum install \


binutils \
gcc \
git \
glibc-static \
gzip \
libbsd \
libcurl \
libedit \
libicu \
libsqlite \
libstdc++-static \
libuuid \
libxml2 \
zlib-devel \
tar \
tzdata

You can now extract the downloaded archive using the tar command. This also can take a
little time:

# untar the downloaded archive using:


# tar xzf <tar-filename>

# example:
tar -xzf swift-5.7.2-RELEASE-amazonlinux2.tar.gz

This creates an extracted-folder <VERSION><PLATFORM> in the same place as your archive


(swift-5.7.2-RELEASE-amazonlinux2 in this example).

The last step is to export the location of the binary. The full pathname followed by /usr/bin is
needed here. We need to put this into the Bash Pro le so it'll load next time you log in:

13
fi
fi
fi
fi
fi
## add the PATH command to the Bash Profile
# echo 'export PATH="<extracted-folder-full-pathname>/usr/bin:${PATH}"' >>
~/.bash_profile
## reload the Bash Profile
# source .bash_profile

# example:
echo 'export PATH="$HOME/swift-5.7.2-RELEASE-amazonlinux2/usr/bin:${PATH}"' >>
~/.bash_profile
source .bash_profile

Now you should be able to use Swift on either Mac or Linux.

VERIFYING SWIFT VERSION


You can verify the installation by entering the following into the command line:

swift --version

swift-driver version: 1.62.15 Apple Swift version 5.7.2 (swiftlang-5.7.2.135.5


clang-1400.0.29.51)
Target: arm64-apple-macosx13.0

And you can optionally remove the tar archive (you'll probably never need it again, but it can
always be re-downloaded if needed):
# rm <tar-filename>

# example:
rm swift-5.7.2-RELEASE-amazonlinux2.tar.gz

Now you're ready to write some Swift code.

THE SWIFT VERSION MANAGER


That was pretty easy, and now I'd like to show you one more tool since it's a cross-platform
solution to manage Swift versions on your operating system.

Swift Version Manager (also called swiftenv) allows you to easily install and switch between
multiple versions of Swift. It can be installed on both platforms, you just have to clone it:

git clone https://github.com/kylef/swiftenv.git ~/.swiftenv

The next step is to con gure the environment .

If you're using Linux, the default shell is probably Bash, as it is with Amazon Linux 2. You can
enter the following commands to set up the environment for swiftenv:

echo 'export SWIFTENV_ROOT="$HOME/.swiftenv"' >> ~/.bash_profile


echo 'export PATH="$SWIFTENV_ROOT/bin:$PATH"' >> ~/.bash_profile
echo 'eval "$(swiftenv init -)"' >> ~/.bash_profile
source .bash_profile

14
fi
ZSH is the default shell from macOS 10.15, so you can run the following command to set up
the environment for swiftenv on macOS:

echo 'export SWIFTENV_ROOT="$HOME/.swiftenv"' >> ~/.zshenv


echo 'export PATH="$SWIFTENV_ROOT/bin:$PATH"' >> ~/.zshenv
echo 'eval "$(swiftenv init -)"' >> ~/.zshenv
source .zshenv

Now you can list all the available Swift language versions:

swiftenv install --list

Run this command to install a speci c Swift version:

swiftenv install <version>

The swiftenv command can do even more, it also supports local Swift versions so you can
have multiple installation on your machine. The o cial website has a complete manual.

MY FIRST VAPOR APP


The Swift Package Manager (SPM) is an open-source tool that can help you manage and
distribute Swift code as packages; it's part of the Swift infrastructure so you don't need to
install anything else. The Swift Package Manager helps you by leveraging the need to
compile and link individual source les, manage dependencies, versioning, etc. SPM works
both on Linux and macOS (also from Swift 5.4 it's supported on Windows too).

You can kick o a new Swift project by using SPM, so let's create a new myProject directory
and initialize a package.

swift package init --type=executable

This command will create a new executable project. Let's take a look at the le structure.
.
!"" Package.swift
!"" README.md
!"" Sources
# $"" myProject
# $"" myProject.swift
$"" Tests
$"" myProjectTests
$"" myProjectTests.swift

The Sources folder will be used as a source for building by default. Each target has its
dedicated place inside the Sources directory. This is where you can put your Swift les. In
our case, the myProject target has only one source le called myProject.swift. This is the
main entry point for our executable myProject target.

15
ff
fi
fi
ffi
fi
fi
fi
I don't want to talk too much about the Tests directory for now: you should only know that
source les for the test targets are located under the Tests folder. There's a dedicated
chapter that explains everything about building tests using Vapor's new test framework.

You can build and run the app with the following command:

swift run myProject

This will print out the famous "Hello, world!" text. You can also run the project from Xcode by
opening the Package.swift le; alternatively, you can generate a dedicated project le. You
can read more about the di erences in a few sections below.

UNDERSTANDING THE PACKAGE MANIFEST FILE


Manifest les always start with a package tool version declaration line. Next, you have to
import the PackageDescription framework, that'll allow you to de ne some ne details
about your package. A package de nition always should have a name and some targets to
build.

Optionally, you can shrink down the supported versions by adding a platforms parameter.
Vapor 4 only supports macOS 10.15 or newer, so it's required to have this in your package
le. There are three other important sections in the package manifest.

A package can have external dependencies. You can specify those under the dependency
section by adding local or remote git repository URLs. You should also explicitly tell the
required package version. Every package should follow the semantic versioning convention.
The version can be used from a speci c one including only minor changes, you can use
branches, or you can exactly tell which one you'd like to use. There are lots of possibilities to
set just the right one you need.

The package may also have its own targets. These are basic building blocks for modules or
test suites: there are standard (library) targets, executable targets, and test targets. Targets
can depend on other targets, or products from external dependencies. You can also set the
path of the target on the disk, exclude source les from the build process, or specify the
ones you want to build. Targets can have custom build con gurations: there's an option to
pass around c, cxx, swift, and linker settings.

The last component of a package is the products section. A product is made of target
dependencies. Currently, you can build two types of products: (1) an executable is a binary
that you can run using the command line; and (2) a library product is something that others
can include as a dependency. You can create both static and dynamic libraries.

HELLO VAPOR
It is time to start using Vapor, so let me show you a working example of integrating the library
into a brand new executable package. We just have to add the framework as a dependency.
The order of the dependencies doesn't matter if you add more third parties.

/// FILE: Package.swift

16
fi
fi
fi
ff
fi
fi
fi
fi
fi
fi
fi
fi
// swift-tools-version:5.7
import PackageDescription

let package = Package(


name: "myProject",
platforms: [
.macOS(.v12)
],
products: [
.executable(name: "myProject", targets: ["myProject"]),
],
dependencies: [
.package(
url: "https://github.com/vapor/vapor",
from: "4.70.0"
),
],
targets: [
.executableTarget(name: "myProject", dependencies: [
.product(name: "Vapor", package: "vapor")
]),
.testTarget(name: "myProjectTests", dependencies: ["myProject"]),
]
)

Now, if you edited the Package.swift le from Xcode, you just have to save the le and
Xcode will start setting up the required dependencies. This can take a while, but you can
track the progress on the sidebar.

If you edited the Package.swift le in the Linux terminal, you can update the package
dependencies, and fetch all the other required dependencies using:

swift package update

Now let's change the myProject.swift le.

/// FILE: Sources/myProject/myProject.swift


import Vapor

@main
public struct myProject {

public static func main() throws {


let env = try Environment.detect()
let app = Application(env)
defer { app.shutdown() }
app.get { req in "Hello Vapor!" }
try app.run()
}
}

This little snippet is just enough to make a simple web server that returns with a hello output.
We import the Vapor framework, then we try to detect the work environment based on the
input arguments and other environment variables from the process info. We simply de ne an
application object that's our web server instance. We have to shut it down once the app is
nished running: we can do this in a defer block. We also have to register a route, so if
someone requests the address of our server, we can respond with the "Hello Vapor!" text.
The nal step is to run the app; this starts our app listening on the default 8080 port for a
request from a browser.

If you press the play button in Xcode, you can run your very rst Vapor application. Or you
can build and run the app using the Linux command line:

17
fi
fi
fi
fi
fi
fi
fi
fi
swift run myProject &

The terminal output should look like this:

$ swift run myProject &


[1]+ Running swift run myProject &
[0/0] Build complete!
2022-02-04T21:36:54+0000 notice codes.vapor.application : Server starting on http://
127.0.0.1:8080
<return>
$

The run command builds the package rst, then executes the myProject product in the
background, that's what the ampersand at the end of the command means.

It'll take some time to build everything the rst time you build, but after the rst time, only
sources that change will need to be rebuilt, and builds will go fast. Alternatively, you can
build the app rst, then run it from the build folder by hand:

swift build
./.build/debug/myProject &

The terminal should look like this:


$ swift build
[4/4] Build complete!
$ ./.build/debug/myProject &
[1] 9893
$ 2022-02-04T22:02:30+0000 notice codes.vapor.application : Server starting on http://
127.0.0.1:8080
<return>
$

The line "[1] 9893" says that the app is running as background job 1, and the PID is 9893.

To test if your application is working, use curl in the foreground:

$ curl localhost:8080
Hello Vapor! # Server response
$

To stop the server, bring it to the foreground, then use the CTRL + C key combination.

$ fg %1
swift run myProject # Command output
^C
$ jobs
$

Notice that the jobs command produces no response because we stopped the server (which
validates that we did stop the server).

If you try to repeat the build & run commands without stopping an app that's running on port
8080, you'll get a lengthy error with "Address already in use (errno: 98)" in it.

If you have myProject running on a local Linux or macOS computer that has a browser, visit
http://localhost:8080/ in your browser. You should see the Hello Vapor! message.

Congratulations you just made your very rst Vapor application.

18
fi
fi
fi
fi
fi
THE VAPOR TOOLBOX
Getting started with Vapor using the Swift Package Manager isn't the only way to bootstrap a
new Vapor project. There's a project called Vapor toolbox which you can install to save you
some time. You can build the toolbox from source on all the supported platforms:

git clone https://github.com/vapor/toolbox.git


cd toolbox
git checkout <desired version>
swift build -c release --disable-sandbox
mv .build/release/vapor /usr/local/bin

You can nd the desired version list here. Alternatively, you can use the homebrew package
manager if you have macOS with homebrew installed.

brew install vapor/tap/vapor

Feel free to remove the previously created Swift Package Manager (SPM) project, myProject.
We're going to use the Vapor toolbox to generate a new one and we'll use this from now on.

cd ~
rm -R myProject
vapor new myProject

It'll ask you about Fluent, that's the ORM layer, but for now, you can answer no and generate
the starter project without database connection support.

It'll also ask you about Leaf, that's Vapor's o cial template engine, but we'll be using
SwiftHtml, so answer no to that question also.

You'll see the vapor toolbox creating the les:

Generating project files


+ Package.swift
+ main.swift
+ configure.swift
+ routes.swift
+ .gitkeep
+ AppTests.swift
+ Dockerfile
+ docker-compose.yml
+ .gitignore
+ .dockerignore
Creating git repository
Adding first commit

followed by the Vapor logo.

The toolbox is a convenient helper tool for creating new projects. You can create your own
templates by forking the original template repository, but that's not necessary unless you
want to contribute by submitting changes through pull requests (PRs for short).

The vapor toolbox also provides shortcuts and assistance for common tasks. You can also
use the vapor build command to build your project and vapor run to execute it. This comes
useful if you don't want to mess around with make les or interact directly with the Swift

19
fi
fi
ffi
fi
Package Manager tool. You can enter vapor --help if you want to learn more about the
toolbox.

SETTING UP CUSTOM WORKING DIRECTORIES


If you run your project through Xcode, you need to set up a custom working directory;
otherwise, your application will look for assets in a cursed place called DerivedData. This
can cause some issues if you're using a template engine or the public le serving
middleware with the default con g since the system won't nd proper routes. To x this you
just click your target name, right next to the stop button, and select the Edit Scheme... menu
item.

Select the Run menu item on the left and click on the Options tab.

Click on the Use custom working directory checkbox and select the folder you want to use.
Now if you build again, the framework will use the selected directory to load resource les;
you should notice that the related warning disappeared from the console output also.

AVOIDING ADDRESS ALREADY IN USE ERRORS IN XCODE


Sometimes when you build and run the project, things can go wrong. One common problem
is the famous "address already in use" error message. This can happen when your previous
server instance is still running and listening on the same port you want to re-use. This can be
quite annoying, because you have to use the Activity Monitor app to kill the running (Run)

20
fi
fi
fi
fi
fi
instance, or you have to use the command line to kill the process; but fortunately, there's a
way better solution.

We can create a Pre-action script for the Run scheme through the Edit Scheme... menu. This
script can check whether some application is listening on a given port and automatically kill
it, if necessary. This is the nal script that you can use:

lsof -i :8080 -sTCP:LISTEN |awk 'NR > 1 {print $2}'|xargs kill -15

The lsof command can check if something is listening on the 8080 port, with the awk
command we can print the process identi er from the output of the lsof command, and
nally, we can pass that identi er with xargs to the kill command that'll terminate the app.

It's kind of a "brute force" approach, but this way we can eliminate this address-related issue
when using Xcode. You can also create more pre-action and post-actions scripts if you want,
but I like this one because this address is already in use errors will happen many times
during development. Just be careful and don't unintentionally shut down other processes
that are listening on di erent ports.

PACKAGE.SWIFT VS GENERATING XCODE PROJECT FILES


As I mentioned before, every package has a manifest le called Package.swift. If you have
installed Xcode, you can double-click on it to open the project and edit the contents of the
package directly from the IDE. This is convenient since Xcode has built-in syntax highlighting
and auto-completion for Swift source les. It can automatically resolve package
dependencies, so you don't have to manually run the update command from the terminal.

21
fi
ff
fi
fi
fi
fi
fi
You can also generate a new Xcode project le based on the manifest by running:

swift package generate-xcodeproj

This will create a regular .xcodeproj le that you can use, but you should note that this
command is now being marked as a deprecated function and eventually it'll be completely
removed from SPM. Currently, running vapor update -y is almost equivalent to executing
both the swift package update and the swift package generate-xcodeproj commands after
one another. The real question is, what are the smaller di erences between the two
approaches, and should you even generate an Xcode project le anymore?

If you generate a .xcodeproj le, your dependencies are going to be linked dynamically, but
if you're using the Package.swift le, the system will use static linking. Don't worry too much
about this: just open the manifest le by double-clicking on it. Dynamic linking can cause
some issues if you're planning to use a package with a reserved system name, such as Ink
by John Sundell. If you're facing similar issues with the generated Xcode project you should
go with static linking instead. As you can see, the nal answer is this: you should always
open the Package.swift manifest le and stop generating Xcode projects.

ANATOMY OF A VAPOR PROJECT


Understanding how a Vapor project is built up is the most important thing for a beginner.
Let's examine the project template les.
.
!"" docker-compose.yml
!"" Dockerfile
!"" Package.swift
!"" Sources
# !"" App
# # !"" configure.swift
# # !"" Controllers
# # $"" routes.swift
# $"" Run
# $"" main.swift
$"" Tests
$"" AppTests
$"" AppTests.swift

I'll quickly walk you through everything. The very rst di erence, compared to our hand-
made package, is that the project template contains two major targets. The rst one is called
App and the second one is the Run target.

You'll nd the source code for every target inside the Sources directory. The Run executable
target is the beginning of everything. It'll load the App library and start the Vapor backend
server with proper con guration and environmental variables. It contains just one single
main.swift le that you can execute.

The App target is where you put your actual backend application code. It's a library package
by default that you can import inside the Run executable target. The application itself uses
the environment to detect which mode is most desirable to run. This can be one of the
following: production, development, or testing. This is one of the main reasons why Vapor
22
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
ff
ff
fi
fi
apps have a dedicated Run target. The library approach helps to re up the server in
di erent modes.

There are two important les in the Sources/App folder that we have to examine closely.

The con gure.swift le contains all the major con gurations for your application. This is the
place where you can customize your application. This is where you should register all the
various services, use middlewares, set the router object, etc. For example, if you want to use
a database connection, a static le hosting service, or a template engine you can set them
up using the public con gure function.

Services allows you to register, con gure, and initialize anything you might need in your
application. Services are the "low-level" building blocks in Vapor; the service framework is a
dependency injection (also called inversion of control) implementation for Vapor. Most of the
underlying components are written as a service. The router is a service, the middleware
system works as a service, database connections are services, and even the HTTP server
engine is implemented as a service.

This service layer is incredibly powerful because you can con gure or replace anything
inside your con guration le; there are only a few hardcoded elements, but everything can
be customized. In Vapor 4, the dependency injection API is entirely based on Swift
extensions; you can usually reach services as properties on the app. This provides us with
an extra layer of security. Letting the compiler do the hard work is always nice, plus this way,
services are easier to discover, since the type system usually knows everything.

The routes.swift le is where you can add the actual routes for your router. Routing is simply
connecting URL path components to request handlers. In other words, routing refers to how
an application's endpoints respond to client requests. We can say that routing is the service
that connects your code with the API endpoints. You can de ne these connections inside the
routes function. In the template project, for example, the hello route means that we should
respond to any incoming HTTP GET request with the "Hello, world!" string as a response.
The status code will be 200, of course, and the returned string is the body. If you don't know
much about how the network layer works, please just read at least this HTTP wiki page.

There are just a few more things that you should know about the Vapor architecture. If you
create a new project and select to use Fluent, you'll see that the template project contains
three additional les and both the router and con guration les are a little bit longer.

Controllers are code organization tools. With their help, you can group related API
endpoints. In the sample project, there's a TodoController that's responsible for providing
CRUD response handlers for the Todo models. The router connects the endpoints by using
this controller, and the controller can query (create, request, update, delete) the appropriate
models using the available database connection.

Models represent database entries related to the Fluent database abstraction (ORM) library.
In the sample project, the Todo model de nes the name of the database schema as a static
property. Each eld in the database table has a corresponding property in the entity. These
properties are marked with a special thing called property wrappers. Through these property
wrappers, you can customize the name and the behavior of the database columns.

Migrations can create or alter the scheme of the database. Models are stored in prede ned
database tables, these migration scripts help you to manage the underlying table de nition.
For example, if you need to introduce a new eld in a model, you can alter your database
according to your needs by implementing a new migration. You also have to start the server
with a custom argument to perform a migration.

23
ff
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
We'll learn a lot more about controllers, models, and migrations in the upcoming chapters.

There are usually two more folders in the project directory. The Public folder is the place for
all the publicly available assets, such as stylesheets (CSS les), JavaScript code, or images.
You should keep this folder, alongside the Resources folder, right under the working
directory of your project, otherwise, the system won't be able to load speci c asset les.

For example, the Resources directory is the default location of view template les. The
SwiftHtml template engine uses the Views folder inside the Resources folder by default to
look up template les. We're also going to store the local database le under the Resources
directory.

You can create these folders now if they don't exist yet because we'll need them shortly.

cd ~/myProject
mkdir Public
mkdir Resources
mkdir Sources/App/Models
mkdir Sources/App/Migrations

The nal project structure looks like this:

.
!"" docker-compose.yml
!"" Dockerfile
!"" Package.swift
!"" Public
!"" Resources
!"" Sources
# !"" App
# # !"" configure.swift
# # !"" Controllers
# # !"" Migrations
# # !"" Models
# # $"" routes.swift
# $"" Run
# $"" main.swift
$"" Tests
$"" AppTests
$"" AppTests.swift

SUMMARY
By now, at the end of this chapter, we've learned how to set up the work environment to
create, build, and run Vapor projects. Swift, and all the developer tools, are deeply
integrated into Xcode, but using the Swift Version Manager can be the best approach to
manage toolchain installations. Bootstrapping a new Vapor project is simple using the Vapor
toolbox, but since the Swift Package Manager is a key component, and it's going to be used
more and more on all Apple platforms, it's highly recommended that you get familiar with it.
That's why we've learned a bit about the package manifest le format and the available
commands. In the very last part, we've talked about the architecture of a Vapor-based
backend application, so we're ready to build something more interesting.

24
fi
fi
fi
fi
fi
fi
fi
fi
CHAPTER 3:
GETTING STARTED WITH SWIFTHTML

In this chapter we're going to build our rst website using the SwiftHtml library. We're going
to generate HTML code through Swift by creating template les using a Domain Speci c
Language (DSL). You'll learn about how to connect SwiftHtml with Vapor and how to render
HTML by using context variables to provide additional template data. You'll learn about the
syntax of SwiftHtml, how to iterate through objects, how to check optional variables, and
how to extend a base template and provide a reusable framework for our website. We'll
build a simple blog layout with a post list and detail pages.

RENDERING TEMPLATES USING SWIFTHTML


The SwiftHtml library has multiple library products. The core library is called SwiftSgml,
which is an abstract interface for the other SGML-based libraries. The SwiftHtml product
contains most of the necessary HTML tags, the SwiftSvg library can help you to dynamically
build SVG tags, the SwiftSitemap product can generate a sitemap.xml le and SwiftRss
dependency can help you with proper RSS feed generation.

You can use SwiftHtml to generate dynamic HTML pages for a front-end website. Using a
DSL-based approach has its bene ts: rst of all, you don't have to write HTML code by hand,
but you can use Swift and take advantage of the compiler to catch errors. Separating the
template layer and using a DSL is always a good thing, and you can reuse the template les
and keep away the view layer from the rest of your business logic.

Using SwiftHtml is relatively simple. If you're familiar with the HTML standard it's going to be
very straightforward to work with this small utility library. SwiftHtml tries to follow the
standards as much as possible, so hopefully, it's going to feel quite natural to build your
templates. It also gives you type safety, so you won't be able to misspell a tag or misplace a
closing tag.

Let's continue with the Vapor toolbox-based example and alter the contents of the package.
We need to add the SwiftHtml package as a dependency to our Package.swift le.

/// FILE: Package.swift

// swift-tools-version:5.7
import PackageDescription

let package = Package(


name: "myProject",
platforms: [
.macOS(.v12)
],
dependencies: [
.package(
url: "https://github.com/vapor/vapor",
from: "4.70.0"

25
fi
fi
fi
fi
fi
fi
fi
fi
),
.package(
url: "https://github.com/binarybirds/swift-html",
from: "1.7.0"
),
],
targets: [
.target(name: "App", dependencies: [
.product(name: "Vapor", package: "vapor"),
.product(name: "SwiftHtml", package: "swift-html"),
.product(name: "SwiftSvg", package: "swift-html"),
]),
.executableTarget(name: "Run", dependencies: ["App"]),
.testTarget(name: "AppTests", dependencies: [
.target(name: "App"),
.product(name: "XCTVapor", package: "vapor"),
])
]
)

Now you should run the swift package update command again, or wait until Xcode fetches
the new package dependencies. When the process is completed, we should be ready to
render HTML les with just a few simple lines in the routes.swift le:

/// FILE: Sources/App/routes.swift

import Vapor
import SwiftHtml

func routes(_ app: Application) throws {


app.get { req async in
"It works!"
}

app.routes.get("hello") { req -> Response in


let doc = Document(.html) {
Html {
Head {
Title("Hello, World!")
}
Body {
H1("Hello, World!")
}
}
}
let body = DocumentRenderer(
minify: false,
indent: 4
)
.render(doc)
return Response(
status: .ok,
headers: [
"Content-Type": "text/html; charset=utf-8"
],
body: .init(string: body)
)
}
}

Alter the con guration le by enabling the FileMiddleware, so Vapor can serve public les
from a directory, please note that you might have to create this directory later on.

/// FILE: Sources/App/con gure.swift

import Vapor

public func configure(_ app: Application) throws {

26
fi
fi
fi
fi
fi
fi
app.middleware.use(
FileMiddleware(
publicDirectory: app.directory.publicDirectory
)
)

try routes(app)
}

If you don't have a Public directory under your project folder, please create one, since we're
going to place our assets there later on. This is also a good time to create other folders we'll
use during this chapter. We're going to create two modules inside the App directory.

Note: A Module is a common interface that can boot a collection of components required to
implement a particular function of the application. For example, in a CRUDS module, there's
a page to show the starting index of existing records (S), then one to create a new record \
(C), one to update an existing record (U), and one to save the updated record... well, you get
the idea. All the code for a given functionality would usually be together in one module. I'm
not saying that it would be in one le: on the contrary, I'm saying that the sub-directories
and Swift les comprising the functionality would be under one directory. The examples here
are Sources/App/Modules/Blog and Sources/App/Modules/Web. (See the tree structure
below.)

Use the commands:

cd ~/myProject
mkdir -p Public
mkdir -p Public/css
mkdir -p Public/img
mkdir -p Public/img/posts
mkdir -p Public/js
mkdir -p Resources
mkdir -p Sources/App/Controllers
mkdir -p Sources/App/Template
mkdir -p Sources/App/Models
mkdir -p Sources/App/Modules
mkdir -p Sources/App/Modules/Web
mkdir -p Sources/App/Modules/Web/Controllers
mkdir -p Sources/App/Modules/Web/Templates
mkdir -p Sources/App/Modules/Web/Templates/Html
mkdir -p Sources/App/Modules/Web/Templates/Contexts
mkdir -p Sources/App/Modules/Blog
mkdir -p Sources/App/Modules/Blog/Controllers
mkdir -p Sources/App/Modules/Blog/Templates
mkdir -p Sources/App/Modules/Blog/Templates/Contexts
mkdir -p Sources/App/Modules/Blog/Templates/Html
mkdir -p Sources/App/Middlewares

Your directory structure should look like this now:

.
!"" Dockerfile
!"" Package.resolved
!"" Package.swift
!"" Public
# !"" css
# !"" img
# # $"" posts

27
fi
fi
# $"" js
!"" Resources
!"" Sources
# !"" App
# # !"" Controllers
# # !"" Middlewares
# # !"" Migrations
# # !"" Models
# # !"" Modules
# # # !"" Blog
# # # # !"" Controllers
# # # # $"" Templates
# # # # !"" Contexts
# # # # $"" Html
# # # $"" Web
# # # !"" Controllers
# # # $"" Templates
# # # !"" Contexts
# # # $"" Html
# # !"" Template
# # !"" configure.swift
# # $"" routes.swift
# $"" Run
# $"" main.swift
!"" Tests
# $"" AppTests
# $"" AppTests.swift
$"" docker-compose.yml

A middleware is a function that will be executed every time before the request handler. So in
our case, if the browser asks for a le such as a stylesheet, a script, or an image, the
FileMiddleware can look it up in the public directory. If the le exists, the content will be
returned as a response. This is great for serving static assets, but please note that
everything inside the con gured directory will be publicly available through the server, so
don't place sensitive data there.

In the next part of this example, we're simply using a request handler block and the built-in
DocumentRenderer from SwiftHtml to return a Response with the necessary headers. A
response object is something that represents an HTTP response. It has a status code, some
header information, and maybe a body. In our case, we simply set the proper content-type
header for our HTML string output, and we can use a 200 status code to indicate that the
response was OK. The DocumentRenderer simply turns our HTML DSL structure into plain
text; you can also minify your output, or set the indent size if you want.

Although Vapor has an abstract view layer that we could use to render our template les, we
want to have a bit more type safety, so we're going to create our own template renderer.
First of all, we're going to need a reusable template protocol:

/// FILE: Sources/App/Template/TemplateRepresentable.swift

import Vapor
import SwiftSgml

public protocol TemplateRepresentable {

28
fi
fi
fi
fi
@TagBuilder
func render(_ req: Request) -> Tag
}

This interface has only one method that can return a Tag object; the method itself is called
render and it'll receive the current Request object so we'll be able to access it inside our
template les. The next step is to create the actual renderer, which is going to be very similar
to the method that we've already had in our con guration le.

/// FILE: Sources/App/Template/TemplateRenderer.swift

import Vapor
import SwiftHtml

public struct TemplateRenderer {

var req: Request

init(_ req: Request) {


self.req = req
}

public func renderHtml(


_ template: TemplateRepresentable,
minify: Bool = false,
indent: Int = 4
) -> Response {
let doc = Document(.html) {
template.render(req)
}
let body = DocumentRenderer(
minify: minify,
indent: indent
)
.render(doc)
return Response(
status: .ok,
headers: [
"Content-Type": "text/html; charset=utf-8"
],
body: .init(string: body)
)
}
}

The TemplateRenderer can render an HTML template, which is a TemplateRepresentable


object, and we're also going to be able to set additional mini cation and indentation options
when calling the renderHtml method. This method returns with a Response object and it's
using the same principles as we've seen before. The TemplateRenderer has an internal init
method: we won't create this struct here, but instead, we're going to extend the Request
object to get an instance of the renderer.

/// FILE: Sources/App/Template/Request+Template.swift

import Vapor

public extension Request {


var templates: TemplateRenderer { .init(self) }
}

Now if we go back to our router le, we can create a new template and render it using the
req.templates extension.

/// FILE: Sources/App/routes.swift

29
fi
fi
fi
fi
fi
import Vapor
import SwiftHtml

struct MyTemplate: TemplateRepresentable {


let title: String

func render(_ req: Request) -> Tag {


Html {
Head {
Title(title)
}
Body {
H1(title)
}
}
}
}

func routes(_ app: Application) throws {


app.get { req async in
"It works!"
}

app.routes.get("hello") { req -> Response in


req.templates.renderHtml(
MyTemplate(title: "Hello, World!")
)
}
}

As you can see, the MyTemplate struct conforms to the TemplateRepresentable protocol;
we've also introduced a new contextual variable called title. We can pass this context to our
template when we initialize it, and we can access the title in the render method. The request
object is also available in the render method, but at this time, we won't use it.

Finally we can call the req.templates.renderHtml method using our template instance to
return an HTML response.

TEMPLATES AND CONTEXTS


So far, we're good with the template renderer, so now let's tackle one other issue by creating
a reusable index template that's going to be the base of every web page that we're going to
render later on. Since we're going to use a modular approach to build everything, this is why
we created the Modules folder with a Web module inside of it.

We're going to place all of our templates inside the Templates/Html directory; every
template will have an associated context object, and we're going to store those inside a
Templates/Contexts directory.

/// FILE: Sources/App/Modules/Web/Templates/Html/WebIndexTemplate.swift

import Vapor
import SwiftHtml

public struct WebIndexTemplate: TemplateRepresentable {

public var context: WebIndexContext

30
public init(_ context: WebIndexContext) {
self.context = context
}

@TagBuilder
public func render(_ req: Request) -> Tag {
Html {
Head {
Meta()
.charset("utf-8")
Meta()
.name(.viewport)
.content("width=device-width, initial-scale=1")

Link(rel: .shortcutIcon)
.href("/img/favicon.ico")
.type("image/x-icon")
Link(rel: .stylesheet)
.href("https://cdn.jsdelivr.net/gh/feathercms/[email protected]
beta.44/feather.min.css")
Link(rel: .stylesheet)
.href("/css/web.css")

Title(context.title)
}
Body {
Main {
Section {
H1(context.message)
}
.class("wrapper")
}
}
}
.lang("en-US")
}
}

This le is our index HTML template. If you're familiar with SwiftUI, you should notice that the
render method uses a result builder to create the necessary structure. The syntax itself is
very simple: every single HTML tag is available as a Tag subclass, so the naming convention
is the same. You can add attributes through modi ers and the entire tree will be rendered
using the DocumentRenderer that we've introduced a bit earlier.

Before we move forward, we should talk a bit about CSS. If you don't know much about
HTML & CSS, you should take a look at this HTML Tutorial. This book will focus more on
Swift, but since the templates will contain lots of HTML code you should have some basic
understanding of the fundamentals of frontend development, including both the Hypertext
Markup Language and Cascading Style Sheets.

We're going to use an external stylesheet through a Content Delivery Network (CDN) system.
A CDN allows us to load external resources much faster. The external Feather CSS le is part
of a CMS system, that contains some basic components that we can use to style our
document. If you take a look at our website with this extra stylesheet imported, you should
see that it works nice both in light and dark mode. If you want to know more about the
underlying components, please take a look at the docs.

After the external CSS, we're also going to add one more extra line to a local CSS le
reference. We're going to place our local style overrides into the Public/css/web.css le. In
the Public/css folder, create a web.css le. A CSS is a stylesheet that describes the visual
style of an HTML document. You can learn more about this format using the W3Schools

31
fi
fi
fi
fi
fi
fi
website. Our web.css le will be quite empty for now, since the external stylesheet gives us
pretty much everything we need to display a nice-looking, but still really basic website.

touch Public/css/web.css

We're also going to de ne the context object to use its properties as variables inside our
template le. The context that's used for this template is called WebIndexContext and it's a
relatively simple struct.

/// FILE: Sources/App/Modules/Web/Templates/Contexts/WebIndexContext.swift

public struct WebIndexContext {

public let title: String


public let message: String

public init(
title: String,
message: String
) {
self.title = title
self.message = message
}
}

The nal step is to alter our routes a little bit and return the rendered template as an HTML
response. You can render a template by using the req.templates.renderHtml method; we
just have to initialize our template with a given context.

// FILE: Sources/App/routes.swift

import Vapor
import SwiftHtml

func routes(_ app: Application) throws {

app.routes.get { req -> Response in


req.templates.renderHtml(
WebIndexTemplate(
WebIndexContext(
title: "Home",
message: "Hi there, welcome to my page!"
)
)
)
}
}

We can say that a template context is a type-safe data representation of everything that we
need to render our template le. Of course, the request object is also available inside the
render method and we can use it to get dynamic path components or the currently logged-in
user, but let's talk about these kinds of things later on.

Remember, the render method will convert your template le into an HTML string and set
some extra headers. The Content-Type will be set to text/html, so your browser can render
the page as an HTML website. Run the app using the command line or Xcode; but if you're
using Xcode, de nitely don't forget to set a custom working directory or the server won't
nd your templates and public les. Check the previous chapter if you don't know how to set
up a custom working directory.

32
fi
fi
fi
fi
fi
fi
fi
fi
fi
If your project isn't on your local machine, test the app using curl. The ampersand at the end
of the Swift command will run the compile in the background so that you can use curl in the
foreground.

swift run &


curl localhost:8080

The response you expect is:

<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="/img/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/feathercms/feather-
[email protected]/feather.min.css">
<link rel="stylesheet" href="/css/web.css">
<title>Home</title>
</head>
<body>
<main>
<section class="wrapper">
<h1>Hi there, welcome to my page!</h1>
</section>
</main>
</body>
</html>

To stop the project, bring the job to the foreground, the use CTRL+C.

$ fg %1
# press CTRL+C to stop the process

TEMPLATE HIERARCHY
Splitting up templates is going to be essential if you're planning to build a multi-page
website. We can create reusable parts that you can share and render them later on inside
other template les. In the following example, we're going to create three separate pages.
First, we have to update the index template, since that's going to be reused for the entire
website.

/// FILE: Sources/App/Modules/Web/Templates/Html/WebIndexTemplate.swift

import Vapor
import SwiftHtml
import SwiftSvg

extension Svg {

static func menuIcon() -> Svg {


Svg {
Line(x1: 3, y1: 12, x2: 21, y2: 12)
Line(x1: 3, y1: 6, x2: 21, y2: 6)
Line(x1: 3, y1: 18, x2: 21, y2: 18)
}
.width(24)
.height(24)

33
fi
.viewBox(minX: 0, minY: 0, width: 24, height: 24)
.fill("none")
.stroke("currentColor")
.strokeWidth(2)
.strokeLinecap("round")
.strokeLinejoin("round")
}
}

public struct WebIndexTemplate: TemplateRepresentable {

public var context: WebIndexContext


var body: Tag

public init(
_ context: WebIndexContext,
@TagBuilder _ builder: () -> Tag
) {
self.context = context
self.body = builder()
}

@TagBuilder
public func render(
_ req: Request
) -> Tag {
Html {
Head {
Meta()
.charset("utf-8")
Meta()
.name(.viewport)
.content("width=device-width, initial-scale=1")

Link(rel: .shortcutIcon)
.href("/img/favicon.ico")
.type("image/x-icon")
Link(rel: .stylesheet)
.href("https://cdn.jsdelivr.net/gh/feathercms/[email protected]
beta.44/feather.min.css")
Link(rel: .stylesheet)
.href("/css/web.css")

Title(context.title)
}
Body {
Header {
Div {
A {
Img(src: "/img/logo.png", alt: "Logo")
}
.id("site-logo")
.href("/")

Nav {
Input()
.type(.checkbox)
.id("primary-menu-button")
.name("menu-button")
.class("menu-button")
Label {
Svg.menuIcon()
}
.for("primary-menu-button")
Div {
A("Home")
.href("/")
.class("selected", req.url.path == "/")
A("Blog")
.href("/blog/")
.class("selected", req.url.path == "/blog/")
A("About")
.href("#")
.onClick("javascript:about();")

34
}
.class("menu-items")
}
.id("primary-menu")
}
.id("navigation")
}

Main {
body
}

Footer {
Section {
P {
Text("This site is powered by ")
A("Swift")
.href("https://swift.org")
.target(.blank)
Text(" & ")
A("Vapor")
.href("https://vapor.codes")
.target(.blank)
Text(".")
}
P("myPage &copy; 2020-2022")
}
}

Script()
.type(.javascript)
.src("/js/web.js")

}
}
.lang("en-US")
}
}

One major change: here's the new builder parameter that you can pass for the template le.
It's marked with the @TagBuilder result builder, so this means that you can build an
additional HTML structure when calling the init method, and the nal tag of that result will be
used inside the main section of the index template. It's not that complicated when you see it
in use; you can simply create a new custom body tag for your index template through this
builder attribute.

The template structure itself is similar to our previous version, but we've added a new
header section with a logo, plus some navigation links that'll help us to transition between
the sub-pages. We're using the SwiftSvg library from the SwiftHtml package to render an
inline SVG to represent our navigation menu icon. It's a standard hamburger menu element.

The good news is that you can create even smaller chunks as functions, so for example the
entire navigation can be a standalone template le or just a new method inside the index
template. Just play around with this a bit and try to make it t your needs.

In our case, this index template will be reused across multiple pages, so we don't have to
copy and paste all the generic Swift HTML code that would be the same everywhere. We're
going to ll the body placeholder with some actual tag de ned in other templates, plus
replace the title variable using the context. Please make sure that you remove the message
variable from the WebIndexContext struct since we don't need that anymore.

35
fi
fi
fi
fi
fi
fi
/// FILE: Sources/App/Modules/Web/Templates/Contexts/WebIndexContext.swift

public struct WebIndexContext {

public let title: String

public init(
title: String
) {
self.title = title
}
}

We have to move the message in the routes.swift le into the tag builder.

/// FILE: Sources/App/routes.swift

import Vapor
import SwiftHtml

func routes(_ app: Application) throws {

app.routes.get { req -> Response in


req.templates.renderHtml(
WebIndexTemplate(
WebIndexContext(
title: "Home"
)
) {
P("Hi there, welcome to my page!")
}
)
}
}

We're going to download the site logo and the favicon from the GitHub repository using the
following snippet:

SRC="raw.githubusercontent.com/tib/practical-server-side-swift/main/Assets"
DST="$HOME/myProject/Public/img/"
curl https://$SRC/favicon.ico -o $DST/favicon.ico
curl https://$SRC/logo.png -o $DST/logo.png

As the last component of our index template, we're going to embed some basic javascript
les from the Public/js directory. Please create an empty web.js le; no worries, we'll use
this real soon.

touch Public/js/web.js

Now you should try to run the application.

THE HOME PAGE TEMPLATE


The home page will be really simple, but rst, we're going to create a new
WebHomeContext struct to represent the data that we'd like to render later on inside our
home template.

36
fi
fi
fi
fi
/// FILE: Sources/App/Modules/Web/Templates/Contexts/WebHomeContext.swift

struct WebHomeContext {
let title: String
let message: String
}

Now we can de ne our WebHomeTemplate le. The tricky part is that we're going to render
a WebIndexTemplate with a custom body tag and we're going to feed the index template's
context with the title from the home template context.

/// FILE: Sources/App/Modules/Web/Templates/Html/WebHomeTemplate.swift

import Vapor
import SwiftHtml

struct WebHomeTemplate: TemplateRepresentable {

var context: WebHomeContext

init(
_ context: WebHomeContext
) {
self.context = context
}

@TagBuilder
func render(
_ req: Request
) -> Tag {
WebIndexTemplate(
.init(
title: context.title
)
) {
Div {
Section {
H1(context.title)
P(context.message)
}
.class("lead")
}
.id("home")
.class("container")
}
.render(req)
}
}

It's time to render the entire page. We don't have to set a body parameter anymore using the
context variable in the request handler since it's already de ned in the home template. This
is a major di erence between variables and evaluated blocks. We can say in general that
variables are usually coming from Swift, and blocks will be de ned using templates.

It's possible to create a chain of templates, so for example index ▸ page ▸ welcome. Multi-
level templates are ne, if you follow the same pattern from above you can create a nice
hierarchy for your views, but you can also go the other way around. So for example, you can
create a LeadTemplate with a title and message context, and render that template inside of a
<div> instead of manually placing the same code there again and again. Try to experiment
with this now, but later on, I'll show you examples.

37
ff
fi
fi
fi
fi
fi
MODULE CONTROLLERS
What makes a module? I already mentioned that a Vapor app can have models, controllers,
migration scripts, and many more. A module is something that holds together these
components plus our template and context les. Our very rst module is called Web
because it's responsible for rendering the main pages of our website.

Until now, we've placed everything inside the con gure or routes les, but that's not a very
good approach to separate things. We'll move the entire template render logic from these
les by using a dedicated WebFrontendController object. You can put this controller into a
le with the same name, under a Controllers directory inside the Web module. Usually, most
of the structs and classes have their own dedicated Swift les, you should follow this
convention later on too.

Instead of using request handler completion blocks, you can also create a function that has
the same signature as the block had, and we can connect this method to the route as a
pointer to handle incoming requests. First, this is our new controller.

/// FILE: Sources/App/Modules/Web/Controllers/WebFrontendController.swift

import Vapor

struct WebFrontendController {

func homeView(
req: Request
) throws -> Response {
req.templates.renderHtml(
WebHomeTemplate(
.init(
title: "Home",
message: "Hi there, welcome to my page."
)
)
)
}
}

The next thing that we should do is to make the connection between the router and the
controller. We're not going to simply put everything into the routes or the con g le; instead,
we'll have a standalone Router. If you have lots of routes it's a good idea to split them up
into collections by using the RouteCollection protocol. This protocol has a boot function that
you have to implement and register the routes using the routes object instead of the app.

You can use the same get method on the routes object just like we did before. There are
helper functions de ned on the RoutesBuilder that are available for all the HTTP methods
(get, post, put, delete, etc.). You can also group routes by path components or middleware. A
route group can be used to connect endpoints under the same namespace with similar
functions.

You could also enter a speci c path component as the rst parameter, but in our case, we'll
simply connect our homeView method from the WebFrontendController to the main
endpoint.

/// FILE: Sources/App/Modules/Web/WebRouter.swift

38
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
import Vapor

struct WebRouter: RouteCollection {

let frontendController = WebFrontendController()

func boot(
routes: RoutesBuilder
) throws {
routes.get(use: frontendController.homeView)
}
}

Now we have to boot the router inside the con guration method. This is a nice approach
since you can have multiple routers and register as many as you want. The boot method
needs a route builder, so we can pass the app.routes property, and that'll just work ne.

/// FILE: Sources/App/con gure.swift

import Vapor

public func configure(


_ app: Application
) throws {

app.middleware.use(
FileMiddleware(
publicDirectory: app.directory.publicDirectory
)
)

let router = WebRouter()


try router.boot(routes: app.routes)
}

You don't need the routes.swift le anymore because it was replaced by WebRouter.swift.
Delete it like this:

rm Sources/App/routes.swift

Run the application and you should see a nice little home page rendered by using the two
template les combined. Don't go to the blog page yet: we're going to do that one next.

RENDERING SUB-TEMPLATES
I mentioned that you can render a template inside a template, so let me show you an
example of how to do that. We're going to use quite a lot of links later on, so it makes sense
to create a WebLinkContext object with a label and URL property.

/// FILE: Sources/App/Modules/Web/Templates/Contexts/WebLinkContext.swift

public struct WebLinkContext {

public let label: String


public let url: String

public init(
label: String,

39
fi
fi
fi
fi
fi
url: String
) {
self.label = label
self.url = url
}
}

With a corresponding WebLinkTemplate, we can render our WebLinkContext objects; of


course, we could add more properties, such as style classes or a boolean value to determine
if the link is a blank link or not, but for the sake of simplicity let's just start with a label & URL.
It's a good experiment for you if you'd like to add more options.
/// FILE: Sources/App/Modules/Web/Templates/Html/WebLinkTemplate.swift

import Vapor
import SwiftHtml

struct WebLinkTemplate: TemplateRepresentable {

var context: WebLinkContext

init(
_ context: WebLinkContext
) {
self.context = context
}

@TagBuilder
func render(
_ req: Request
) -> Tag {
A(context.label)
.href(context.url)
}
}

We should also alter the WebHomeContext struct, so we can take advantage of the newly
created link context. We're also going to drop in a new icon property to make our home
page just a bit prettier.

/// FILE: Sources/App/Modules/Web/Templates/Contexts/WebHomeContext.swift

struct WebHomeContext {
let icon: String
let title: String
let message: String
let paragraphs: [String]
let link: WebLinkContext
}

We have to upgrade our home page template to represent the changes that we made
earlier.

/// FILE: Sources/App/Modules/Web/Templates/Html/WebHomeTemplate.swift

import Vapor
import SwiftHtml

struct WebHomeTemplate: TemplateRepresentable {

var context: WebHomeContext

init(

40
_ context: WebHomeContext
) {
self.context = context
}

@TagBuilder
func render(
_ req: Request
) -> Tag {
WebIndexTemplate(
.init(title: context.title)
) {
Div {
Section {
P(context.icon)
H1(context.title)
P(context.message)
}
.class("lead")

for paragraph in context.paragraphs {


P(paragraph)
}

WebLinkTemplate(context.link).render(req)
}
.id("home")
.class("container")
}
.render(req)
}
}

As you can see, we can use the WebLinkTemplate with the link context (that's part of the
home context) and use the render method on the template to return a tag. The returned Tag
object is just like any other tag that we can create by hand, so it's safe to embed one
template inside of another.

Please note that we can still use a regular for loop (also it's possible to use if-else) inside the
template le. This is great because we can iterate through paragraph values and render
them by using the P tag.

/// FILE: Sources/App/Modules/Frontend/Controllers/FrontendController.swift

import Vapor

struct WebFrontendController {

func homeView(req: Request) throws -> Response {


let ctx = WebHomeContext(
icon: "👋 ",
title: "Home",
message: "Hi there, welcome to my page.",
paragraphs: [
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
"Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.",
"Nisi ut aliquip ex ea commodo consequat.",
],
link: .init(
label: "Read my blog →",
url: "/blog/"
)
)

return req.templates.renderHtml(
WebHomeTemplate(ctx)

41
fi
)
}
}

Finally, we have to modify the frontend controller, and of course, we can use some lorem
ipsum text to display some random text inside the body. As you can see, using template
hierarchies is quite simple with SwiftHtml, since you can use a @TagBuilder to provide
additional content for a template, or you can simply render a template inside another.

THE BLOG LIST


Since we're building an app using a modular architecture, we can't simply put blog-related
stu into the Web module. The web module is somewhat special in our case since it
provides us with the main elements to render our website. It contains the index template the
web stylesheet and javascript les too.

That's why we created the module called Blog. Every single module will follow the same
pattern as we created before. This means that we're going to have dedicated routers and
controllers. Before we dive in we're going to create a BlogPost struct to represent our
articles. Make a new Swift le under the Sources/App/Modules/Blog directory.

/// FILE: Sources/App/Modules/Blog/BlogPost.swift

import Foundation

struct BlogPost: Codable {


let title: String
let slug: String
let image: String
let excerpt: String
let date: Date
let category: String?
let content: String
}

Title is the title of the blog post. We're going to use the slug eld to have a nice SEO-friendly
URL for the posts. I've prepared some images that you can grab from the source materials.
Place them under the Public/img/posts directory. The easiest way is to enter the commands
below into your AWS terminal. You can use the same commands on a Mac in a terminal
window.

SRC="raw.githubusercontent.com/tib/practical-server-side-swift/main/Assets"
DST="$HOME/myProject/Public/img/posts"
curl https://$SRC/01.jpg -o $DST/01.jpg
curl https://$SRC/02.jpg -o $DST/02.jpg
curl https://$SRC/03.jpg -o $DST/03.jpg
curl https://$SRC/04.jpg -o $DST/04.jpg
curl https://$SRC/05.jpg -o $DST/05.jpg
curl https://$SRC/06.jpg -o $DST/06.jpg
curl https://$SRC/07.jpg -o $DST/07.jpg
curl https://$SRC/08.jpg -o $DST/08.jpg
curl https://$SRC/09.jpg -o $DST/09.jpg
curl https://$SRC/10.jpg -o $DST/10.jpg

42
ff
fi
fi
fi
We're going to store the name of these under the image eld. Excerpt is going to be
displayed on the list, and post date is the publish date of a given post. Category is an
optional string that we're going to use as a category to group posts together. Content is
going to be displayed on the post detail pages.

How do we store these blog posts? Well, for now, we're going to generate some random
data using the BlogFrontendController to simplify things. In the next chapter, we're going to
use an SQLite database, and later on, we're going to migrate to PostgreSQL storage.

We're going to create a few sample posts by simply using the stride method and map the
indexes to BlogPost types. To uniquely identify every blog post with a slug, we just use a
standard dashed version of the title which will also contain the index value. We'll generate
random date values from the past 60 days for the sample posts. There will be a total of 9
random posts. Finally, everything gets sorted by date, all of this happens inside of a posts
variable.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogFrontendController.swift

import Vapor

struct BlogFrontendController {

var posts: [BlogPost] = {


stride(from: 1, to: 9, by: 1).map { index in
BlogPost(
title: "Sample post #\(index)",
slug: "sample-post-\(index)",
image: "/img/posts/\(String(format: "%02d", index + 1)).jpg",
excerpt: "Lorem ipsum",
date: Date().addingTimeInterval(-Double.random(in: 0...(86400 * 60))),
category: Bool.random() ? "Sample category" : nil,
content: "Lorem ipsum dolor sit amet."
)
}.sorted() { $0.date > $1.date }
}()
}

The BlogFrontendController is responsible for handling all the blog-related routes that are
being publicly available on the web. That's why it's called a frontend controller. We'll use the
same logic later on to create other types of content channels, such as admin controllers and
API controllers.

Now for our blog posts page, we're going to need a new BlogPostsContext struct that we
can use to render a page.

/// FILE: Sources/App/Modules/Blog/Templates/Contexts/BlogPostsContext.swift

struct BlogPostsContext {
let icon: String
let title: String
let message: String
let posts: [BlogPost]
}

43
fi
We should add a new template called BlogPostsTemplate to the project. This le goes
under the Blog/Templates/Html directory. we're going to iterate through the blog posts in
this view and render the available post data.

/// FILE: Sources/App/Modules/Blog/Templates/Html/BlogPostsTemplate.swift

import Vapor
import SwiftHtml

struct BlogPostsTemplate: TemplateRepresentable {

var context: BlogPostsContext

init(
_ context: BlogPostsContext
) {
self.context = context
}

@TagBuilder
func render(
_ req: Request
) -> Tag {
WebIndexTemplate(
.init(title: context.title)
) {
Div {
Section {
P(context.icon)
H1(context.title)
P(context.message)
}
.class("lead")

Div {
for post in context.posts {
Article {
A {
Img(src: post.image, alt: post.title)
H2(post.title)
P(post.excerpt)
}
.href("/\(post.slug)/")
}
}
}
.class("grid-221")
}
.id("blog")
}
.render(req)
}
}

I already mentioned this, but the nice thing about using the third-party Feather CSS
framework is that we get most of the components out of the box. For example, our list will be
responsive, because we're using the grid-221 class.

This means that the grid will use a 2 column layout on desktop and tablet devices and it'll
feature a single column on mobile devices. We have to tweak the standard heading
elements for our posts when we display them in the list; we're going to add one small
change to our web.css le.

/* FILE: Public/css/web.css */

44
fi
fi
#blog h2 {
margin: 0.5rem 0;
}

Now we can set up a request handler for this template inside the controller. Don't remove
anything; just add the func blogView at the end.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogFrontendController.swift

import Vapor

struct BlogFrontendController {

var posts: [BlogPost] = {


stride(from: 1, to: 9, by: 1).map { index in
BlogPost(
title: "Sample post #\(index)",
slug: "sample-post-\(index)",
image: "/img/posts/\(String(format: "%02d", index + 1)).jpg",
excerpt: "Lorem ipsum",
date: Date().addingTimeInterval(-Double.random(in: 0...(86400 * 60))),
category: Bool.random() ? "Sample category" : nil,
content: "Lorem ipsum dolor sit amet."
)
}.sorted() { $0.date > $1.date }
}()

func blogView(
req: Request
) throws -> Response {
let ctx = BlogPostsContext(
icon: "🔥 ",
title: "Blog",
message: "Hot news and stories about everything.",
posts: posts
)
return req.templates.renderHtml(
BlogPostsTemplate(ctx)
)
}
}

The request handler is very similar to the one that we made for the home template, except
that now we use an array of posts as part of the context. We'll also have to create a router
object for the blog module along with the controller. The only route that we're going to
register is going to be the list view for the blog. This goes inside the blog module directory
saved as BlogRouter.swift.

/// FILE: Sources/App/Modules/Blog/BlogRouter.swift

import Vapor

struct BlogRouter: RouteCollection {

let controller = BlogFrontendController()

func boot(
routes: RoutesBuilder
) throws {
routes.get("blog", use: controller.blogView)
}
}

45
As a nal step, we have to register this newly created BlogRouter inside the con g le. We
can simply initiate a new object and put it into the routers array. This way Vapor can boot
both the frontend and the blog router and register all the necessary route handlers.

/// FILE: Sources/App/con gure.swift

import Vapor

public func configure(


_ app: Application
) throws {

app.middleware.use(
FileMiddleware(
publicDirectory: app.directory.publicDirectory
)
)

let routers: [RouteCollection] = [


WebRouter(),
BlogRouter(),
]
for router in routers {
try router.boot(routes: app.routes)
}
}

Run the application and navigate to the /blog/ page, you should see the list of posts.

THE POST ENTRY PAGES

46
fi
fi
fi
fi
The very last thing in this chapter that we're going to accomplish is that we implement a
search engine optimization (SEO) friendly routing for the blog post detail pages. This means
that we're going to use a unique slug as the path of the URL to see the detail page for every
single article. We'll start by creating a new BlogPostTemplate le in the Templates folder.

/// FILE: Sources/App/Modules/Blog/Templates/Html/BlogPostTemplate.swift

import Vapor
import SwiftHtml

struct BlogPostTemplate: TemplateRepresentable {

var context: BlogPostContext

init(
_ context: BlogPostContext
) {
self.context = context
}

var dateFormatter: DateFormatter = {


let formatter = DateFormatter()
formatter.dateStyle = .long
formatter.timeStyle = .short
return formatter
}()

@TagBuilder
func render(
_ req: Request
) -> Tag {
WebIndexTemplate(
.init(title: context.post.title)
) {
Div {
Section {
P(dateFormatter.string(from: context.post.date))
H1(context.post.title)
P(context.post.excerpt)
}
.class(["lead", "container"])

Img(src: context.post.image, alt: context.post.title)

Article {
Text(context.post.content)
}
.class("container")
}
.id("post")
}
.render(req)
}
}

The date is a special variable: since it's stored as a Date value, we can format it and print it
as a human-friendly representation with the help of a custom date formatter. The good news
is that template les are Swift les, so it's really easy to share a global date formatter to use
the same format, but this time a local variable will do just ne.

Apart from the date output, the snippet above follows pretty much the same logic as we had
in the blog template. The context that we used for it (BlogPostContext) contains a simple
post variable.

/// FILE: Sources/App/Modules/Blog/Templates/Contexts/BlogPostContext.swift

47
fi
fi
fi
fi
struct BlogPostContext {
let post: BlogPost
}

In our controller, we have to nd the rst element that has a matching slug with the current
path of our URL string. If there's no match, we can simply redirect the browser to the home
screen, but if there's an article that has the given path, we can render it using the view
system. Add postView to the end of BlogFrontendController.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogFrontendController.swift

import Vapor

struct BlogFrontendController {

var posts: [BlogPost] = {


stride(from: 1, to: 9, by: 1).map { index in
BlogPost(
title: "Sample post #\(index)",
slug: "sample-post-\(index)",
image: "/img/posts/\(String(format: "%02d", index + 1)).jpg",
excerpt: "Lorem ipsum",
date: Date().addingTimeInterval(-Double.random(in: 0...(86400 * 60))),
category: Bool.random() ? "Sample category" : nil,
content: "Lorem ipsum dolor sit amet."
)
}.sorted() { $0.date > $1.date }
}()

func blogView(
req: Request
) throws -> Response {
let ctx = BlogPostsContext(
icon: "🔥 ",
title: "Blog",
message: "Hot news and stories about everything.",
posts: posts
)
return req.templates.renderHtml(
BlogPostsTemplate(ctx)
)
}

func postView(
req: Request
) throws -> Response {
let slug = req.url.path.trimmingCharacters(
in: .init(charactersIn: "/")
)
guard let post = posts.first(where: { $0.slug == slug }) else {
return req.redirect(to: "/")
}
let ctx = BlogPostContext(post: post)
return req.templates.renderHtml(
BlogPostTemplate(ctx)
)
}
}

You can access the path of the URL via the req.url.path property. We need to trim it rst
since we don't care about trailing and leading slashes; next, we can lter our blog posts to
see if there are any that match the given route.

48
fi
fi
fi
fi
This time we'll redirect to the home page if there was no match using a future response.
Otherwise, we'll display the post using the view renderer. Since the redirect method also
returns a Response, it's safe to return with it.

ROUTES AND PATH COMPONENTS


So, we were able to create our controller method; now the only question is: how do we
connect the handler to every single route that can have a possible matching path?

You need to know that you can catch all the routes using a route handler and the .anything
path component. There's also a .catchall case, the only di erence between the two of them
is that anything (*) is just a single match for a path component, but the catch-all (**) case will
catch everything after the rst / character including other sub-paths such as /foo/bar/.

In our case the .anything pattern will be enough, this is how we can use it:

/// FILE: Sources/App/Modules/Blog/BlogRouter.swift

import Vapor

struct BlogRouter: RouteCollection {

let controller = BlogFrontendController()

func boot(
routes: RoutesBuilder
) throws {
routes.get("blog", use: controller.blogView)
routes.get(.anything, use: controller.postView)
}
}

Build and run the application using the command line or Xcode. In your browser window
click on one of the blog posts and hopefully you should be able to read the full article.

49
fi
ff
From an SEO perspective, this approach is nice because of the clean URLs. That's one of the
most important factors during ranking. As a practice you can extend the index template with
some additional meta information; to support rich previews or, as an alternative, you can
move out the lead section and build a custom template for it.

CUSTOM MIDDLEWARES
Now if you enter the blog URL, notice that it'll work with a / su x and without a trailing slash
character. This means that we can access every single URL using two versions of the same
path (e.g. /blog/ vs /blog). We can change this behavior, if needed, by hooking into the
"responder chain".

As I mentioned before, middlewares can hook into requests and alter their behavior. we're
going to place our custom middlewares into a Middlewares folder under the App/
Middleware folder we already created, and add a new ExtendPathMiddleware.swift le to it
with the following contents.

In a modern Vapor application, a middleware should conform to the AsyncMiddleware


protocol. This protocol uses the brand new async/await feature that's available from Swift
5.5. Of course, there's an older Middleware protocol that returns with an EventLoopFuture,
but it's clear to see that we should avoid that because it's way more complicated to work
with futures and promises. Let's just say for now that a function with an async signature is
something that you have to wait for; so for example, the next parameter is an
AsyncResponder, and this is why we have to put the await keyword before the method

50
ffi
fi
when we call it. You can read more about async / await and concurrency on the o cial Swift
website.

/// FILE: Sources/App/Middlewares/ExtendPathMiddleware.swift

import Vapor

struct ExtendPathMiddleware: AsyncMiddleware {

func respond(
to req: Request,
chainingTo next: AsyncResponder
) async throws -> Response {
if !req.url.path.hasSuffix("/") && !req.url.path.contains(".") {
return req.redirect(
to: req.url.path + "/",
redirectType: .permanent
)
}
return try await next.respond(
to: req
)
}
}

When a request arrives at the server, we're going to check if the path of the request URL has
a forward slash su x when it doesn't contain an extension (dot character). If not, we can
simply redirect the client to a path that we would like to see, using a .permanent redirection
type. If the path contains a trailing slash, we can use the Responder object and "pass the
chain" to the next responder.

Think of this as a chain of request handlers that will be called one after another. Every
member of the chain can alter the request object, extend it with additional information (e.g.
authentication) or terminate the execution by sending a response. The nal element in your
chain is usually the request handler that you register with the .get, .post, etc. methods on the
app or router instance.

Now that we de ned a middleware, we still have to register it so it can be part of the chain.
We can do this in the con gure.swift le using the middleware property on our application.

/// FILE: Sources/App/con gure.swift

import Vapor

public func configure(


_ app: Application
) throws {

app.middleware.use(
FileMiddleware(
publicDirectory: app.directory.publicDirectory
)
)

app.middleware.use(ExtendPathMiddleware())

let routers: [RouteCollection] = [


WebRouter(),
BlogRouter(),
]
for router in routers {
try router.boot(routes: app.routes)
}

51
fi
ffi
fi
fi
fi
fi
ffi
}

In Vapor, it's relatively easy to alter the responder chain through middlewares. You can use
middleware for many things, and in this example, we were only scratching the surface. You
need to keep in mind that this little path extension middleware is only good for GET
requests. In a real-world server application, you might want to check the request method
and perform additional checking if you want to use such a middleware.

What about the last menu item? Let's use that empty web.js le that we created at the
beginning of the tutorial. We're going to simply display an alert, but of course, you can use
this template to spice up the website with some fancy animations.

/* FILE: Public/js/web.js */

function about() {
alert("myPage\n\nversion 1.0.0");
}

That's the about menu, nothing serious for now, but I hope that this example gives you a
basic idea about how to import and use javascript les. You can use jQuery or anything else
to make your life better, but in this book, we're only going to write Vanilla JavaScript.

SUMMARY
This chapter was all about getting started with Vapor and the view templates. SwiftHtml is
real easy to start with: the most di cult part is when you have to create the connection
between the library and Vapor. Using a DSL to write type-safe HTML code is nice since the
compiler can catch your errors at build time and you'll make fewer mistakes. We've seen how
you can create modules to separate individual components in your application. Modules are
really powerful code organization tools; using standalone Routers and Controllers helps us
to maintain clean code everywhere. We've also learned about the fundamentals of routing
and played around a little bit with an async middleware. In the next chapter, we'll focus on
persisting blog entries into a local SQLite database using Fluent.

52
ffi
fi
fi
CHAPTER 4:
GETTING STARTED WITH FLUENT

In this chapter, you'll learn about the Fluent ORM framework and the advantages of using
such a tool instead of writing raw database queries. We'll set up Fluent powered by the
SQLite driver, and model our database elds using property wrappers in Swift. We're going
to provide a seed for our database, get familiar with migration scripts, and make some
changes to the website, so it can query blog posts from the local database and render them
using view templates.

SETTING UP FLUENT
Let's start by creating the new folders we’ll use during this chapter. You can use the
commands below to add them:

cd ~/myProject
mkdir -p Sources/App/Modules/Blog/Database
mkdir -p Sources/App/Modules/Blog/Database/Models
mkdir -p Sources/App/Modules/Blog/Database/Migrations
mkdir -p Sources/App/Modules/Blog/Objects
mkdir -p Sources/App/Framework

Next, we have to add Fluent as a dependency. Fluent is an abstraction layer over database
engines. The main implementation is separated into a standalone Swift package, and comes
with several database drivers; each of them has a distinct SPM repository. In this example,
we're going to work with the SQLite driver.

Let's add the new dependencies into the Package.swift le.

/// FILE: Package.swift

// swift-tools-version:5.7
import PackageDescription

let package = Package(


name: "myProject",
platforms: [
.macOS(.v12)
],
dependencies: [
.package(
url: "https://github.com/vapor/vapor",
from: "4.70.0"
),
.package(
url: "https://github.com/vapor/fluent",
from: "4.4.0"
),
.package(
url: "https://github.com/vapor/fluent-sqlite-driver",
from: "4.1.0"
),

53
fi
fi
.package(
url: "https://github.com/binarybirds/swift-html",
from: "1.7.0"
),
],
targets: [
.target(name: "App", dependencies: [
.product(name: "Vapor", package: "vapor"),
.product(name: "Fluent", package: "fluent"),
.product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"),
.product(name: "SwiftHtml", package: "swift-html"),
.product(name: "SwiftSvg", package: "swift-html"),
]),
.executableTarget(name: "Run", dependencies: ["App"]),
.testTarget(name: "AppTests", dependencies: [
.target(name: "App"),
.product(name: "XCTVapor", package: "vapor"),
])
]
)

Update the packages in Linux, or wait for Xcode to be ready after the resolution process,
then you can start using Fluent by importing the frameworks. The Fluent package contains
the extensions for Vapor; FluentKit is the actual abstraction layer and the
Fluent[database]Driver (in this case FluentSQLiteDriver) is the implementation of the given
db driver. You don't have to import FluentKit explicitly.

/// FILE: Sources/App/con gure.swift

import Vapor
import Fluent
import FluentSQLiteDriver

public func configure(


_ app: Application
) throws {

let dbPath = app.directory.resourcesDirectory + "db.sqlite"


app.databases.use(.sqlite(.file(dbPath)), as: .sqlite)

// ...

In the con gure le, you have to import both Fluent and the FluentSQLiteDriver package,
but this is the only place where you have to import the speci c driver implementation. The
SQLite database driver can save data to a memory or le storage, and we're going to
con gure it to persist everything into a db.sqlite le under the Resources directory for now.

The good thing is that this is the only le where you have to interact with the underlying
driver. From now on you just have to import the abstract Fluent module everywhere else and
you're ready to work with the database through the ORM framework. If you want to switch to
a new database driver you just have to change the con g. We'll do this in a later chapter.

WORKING WITH MODELS


The next step to model our blog database is to create an entity that's going to represent the
categories for the blog posts. In their most basic form, models describe table rows in the

54
fi
fi
fi
fi
fi
fi
fi
fi
fi
database. Writing a model de nition is an easy task using the Fluent framework: we just
have to conform to the Model protocol.

We're going to place everything that's database-related under the corresponding module
inside a Database folder. The BlogCategoryModel.swift le should be placed inside the
blog module under a Models directory inside the Database folder and it should look like this:

/// FILE: Sources/App/Modules/Blog/Database/Models/BlogCategoryModel.swift

import Vapor
import Fluent

final class BlogCategoryModel: Model {

static let schema = "blog_categories"

struct FieldKeys {
struct v1 {
static var title: FieldKey { "title" }
}
}

@ID() var id: UUID?


@Field(key: FieldKeys.v1.title) var title: String
@Children(for: \.$category) var posts: [BlogPostModel]

init() { }

init(id: UUID? = nil, title: String) {


self.id = id
self.title = title
}
}

You'll need to create a schema property that has the same name as the database table
where you're going to store these entities. The name of each row is going to be de ned
using a new FieldKey type. I always prefer to have a struct with all the keys as static
variables grouped by the current version. I'm using a simple incremental versioning here;
during the migration process, we're going to use these keys to create the underlying
database scheme. As your models evolve, keys can change and the versioning helps you to
track what has been changed, so it's going to be easier to keep track of the eld keys.

Fields are Swift properties using a property wrapper to denote identi ers for queries and
more complex mappings. This means that database elds are marked with special property
wrappers coming from the Fluent framework. The @ID wrapper is made for creating unique
identi ers. The @Field wrapper can be used to set up regular elds with the corresponding
keys as database columns. Both @Children and the @Parent wrappers are used to create
links between relationships. Every property wrapper has an associated eld key.

Models should have a unique identi er eld. In the latest version of Fluent, UUID is the
preferred type for this purpose, but you can also use Int or something else; however, I
wouldn't recommend that. Currently, UUID is the only id type that's working with all drivers.

As a eld, you can use any type from the Swift programming language: you can store strings,
numbers, enums, and even complex JSON objects. You can read more about the supported
types in my article about Fluent. The very last step in the Model instance is to implement the
necessary init methods. Models are always de ned as classes, so you have to create these

55
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
init methods by hand; it'd be worth the trouble to look for some Xcode extension that can
generate them for you.

Based on what we know the BlogPostModel should look like this:

/// FILE: Sources/App/Modules/Blog/Database/Models/BlogPostModel.swift

import Vapor
import Fluent

final class BlogPostModel: Model {

static let schema: String = "blog_posts"

struct FieldKeys {
struct v1 {
static var title: FieldKey { "title" }
static var slug: FieldKey { "slug" }
static var imageKey: FieldKey { "image_key" }
static var excerpt: FieldKey { "excerpt" }
static var date: FieldKey { "date" }
static var content: FieldKey { "content" }
static var categoryId: FieldKey { "category_id" }
}
}

@ID()
var id: UUID?

@Field(key: FieldKeys.v1.title)
var title: String

@Field(key: FieldKeys.v1.slug)
var slug: String

@Field(key: FieldKeys.v1.imageKey)
var imageKey: String

@Field(key: FieldKeys.v1.excerpt)
var excerpt: String

@Field(key: FieldKeys.v1.date)
var date: Date

@Field(key: FieldKeys.v1.content)
var content: String

@Parent(key: FieldKeys.v1.categoryId)
var category: BlogCategoryModel

init() { }

init(
id: UUID? = nil,
title: String,
slug: String,
imageKey: String,
excerpt: String,
date: Date,
content: String,
categoryId: UUID)
{
self.id = id
self.title = title
self.slug = slug
self.imageKey = imageKey
self.excerpt = excerpt
self.date = date
self.content = content
$category.id = categoryId
}

56
}

Note: One thing worth mentioning here is that if you want to use PostgreSQL with Fluent,
you may want to follow Swift camel case naming conventions in your application, but
PostgreSQL only supports snake cased identi ers. This is a super easy problem to solve
with Fluent: put the snake cased PostgreSQL name in the FieldKey de nition (for example,
FieldKey { "image_key" } here), and the camel case name in the Field de nition (for
example, @Field ... var imageKey: String here). The only minor downside to this is that if you
want to do an SQL search through some other app (for example, psql at the command line),
you'll have to enter it using a snake case. This isn't a deal-breaker for me, though.

Each @Field follows pretty much the same pattern, except that we're marking category as a
@Parent, since we want to be able to put multiple posts under each category; this is a one-
to-many relationship where the posts are marked with the @Children wrapper using a key
path from the other object. We're going to talk a lot more about relationships and key paths
later on, but for now, this is just enough to work with. Before we start using these models, we
have to create some migrations.

DATABASE MIGRATIONS
Migration is the process of creating, updating, or deleting one or more database tables. In
other words, everything that alters the database schema is a migration. You should know
that you can register multiple migration scripts and Vapor will run them in the same order
that you add them to the migrations array. I also prefer to version them with a simple v1, v2...
vN su x.

If you're working with SQL databases, you have to create tables with prede ned schemas to
store the data. Migration is the process of this schema creation, and you can use migration
scripts to alter the schema: for example if you introduce a new property on your model or
seed the database with basic entries. In other words, migration is preparing your database
for your models. Fluent keeps track of the migrations, so you don't have to worry about
which ones have been done. Later in the chapter, I'll go over this in more detail.

I'm going to place every single migration version inside a BlogMigrations enum; if there are
too many versions, you can outsource them into separate Swift les through extensions.
Every migration version should conform to the AsyncMigration protocol, which is quite
similar to the AsyncMiddleware, in terms of the naming convention, and it indicates that
Fluent is taking advantage of the new asynchronous APIs.

Async/await is a powerful Swift feature, and as we move forward with this chapter, we're
going to use more and more async methods. It's a new concurrency model for the Swift
programming language, which allows us to work with asynchronous functions in a better
way. If a method needs to perform some sort of asynchronous work (that can be done in the
background without further CPU usage, e.g. I/O operation) our program can perform other
tasks in the meantime. In the past, we've used completion blocks to wait until the function
returns, but that approach can easily lead us to callback hell or we might build the great
pyramid of doom.

57
ffi
fi
fi
fi
fi
fi
With async/await, you can simply mark a function to say that it might or will perform other
tasks and that the ow of execution can be suspended for a while until the function returns
(resumes) with a value. So the async keyword marks the function and the await keyword
simply tells us to wait until something is returned from the asynchronous function. In the
past, we had to use EventLoopFutures and it was quite a bad situation, if you're familiar with
those, you can read the o cial Vapor docs, about how to migrate to the new async/await
model. I highly recommend this: async/await is a way better solution.

Let's take a look at how to create a migration for the blog module.

/// FILE: Sources/App/Modules/Blog/Database/Migrations/BlogMigrations.swift

import Foundation
import Fluent

enum BlogMigrations {

struct v1: AsyncMigration {

func prepare(on db: Database) async throws {


try await db.schema(BlogCategoryModel.schema)
.id()
.field(BlogCategoryModel.FieldKeys.v1.title, .string, .required)
.create()

try await db.schema(BlogPostModel.schema)


.id()
.field(BlogPostModel.FieldKeys.v1.title, .string, .required)
.field(BlogPostModel.FieldKeys.v1.slug, .string, .required)
.field(BlogPostModel.FieldKeys.v1.imageKey, .string, .required)
.field(BlogPostModel.FieldKeys.v1.excerpt, .data, .required)
.field(BlogPostModel.FieldKeys.v1.date, .datetime, .required)
.field(BlogPostModel.FieldKeys.v1.content, .data, .required)
.field(BlogPostModel.FieldKeys.v1.categoryId, .uuid)
.foreignKey(
BlogPostModel.FieldKeys.v1.categoryId,
references: BlogCategoryModel.schema, .id,
onDelete: DatabaseSchema.ForeignKeyAction.setNull,
onUpdate: .cascade
)
.unique(on: BlogPostModel.FieldKeys.v1.slug)
.create()
}

func revert(on db: Database) async throws {


try await db.schema(BlogCategoryModel.schema).delete()
try await db.schema(BlogPostModel.schema).delete()
}
}
}

Building up the structures for models is done through schema builders. On the database
object, you can de ne a schema by providing a name, then you have to list all the elds with
the given types you want to use as a storage space. You can create multiple schemas at
once; the AsyncMigration utilizes the async/await features, so you can simply try await for a
create operation and move to the next one. A migration can also be undone: that's why you
have to implement a revert method.

Finally, you can add constraints to the elds by using the foreignKey method. Constraints
can update your database if a change occurs in a relationship. You can set your personal
preference on delete and update actions. For example, if a parent category gets deleted,

58
fl
fi
ffi
fi
fi
you can delete all the children or set the category of the referencing children to null. You can
also ensure a eld's uniqueness by putting a unique constraint on it.

Now we should turn our previously de ned dummy data into a database seed. We just have
to construct BlogPostModel objects during the migration process. This will look very similar
to the previously de ned posts variable in the blog frontend controller; we continue to work
with randomly generated data, but now everything will be saved to database tables. Create
a seed migration inside the BlogMigrations enum like this:

/// FILE: Sources/App/Modules/Blog/Database/Migrations/BlogMigrations.swift

import Foundation
import Fluent

enum BlogMigrations {

// ...

struct seed: AsyncMigration {

func prepare(on db: Database) async throws {

let categories = (1...4).map { index in


BlogCategoryModel(title: "Sample category #\(index)")
}
try await categories.create(on: db)

try await (1...9).map { index in


BlogPostModel(
id: nil, title: "Sample post #\(index)",
slug: "sample-post-\(index)",
imageKey: "/img/posts/\(String(format: "%02d", index + 1)).jpg",
excerpt: "Lorem ipsum",
date: Date().addingTimeInterval(-Double.random(in: 0...(86400 *
60))),
content: "Lorem ipsum dolor sit amet.",
categoryId: categories[Int.random(in: 0..<categories.count)].id!
)
}
.create(on: db)
}

func revert(on db: Database) async throws {


try await BlogPostModel.query(on: db).delete()
try await BlogCategoryModel.query(on: db).delete()
}
}
}

There's a parent-child relation between the posts and categories: that's why we have to
save the category rst. We can only form relationships between database objects if the
referenced identi er already exists. Fortunately Fluent is smart enough and the system will
associate an identi er to the saved category pointers automatically so we can use them
right away.

In the prepare function, rst, we create 3 random category objects, and then we save them
into the categories table. After they're created, an id object will be associated with the saved
models automatically. We're going to use the array of categories and randomly pick a
category identi er for each blog post.

59
fi
fi
fi
fi
fi
fi
fi
fi
The very last step is to register our migration scripts so the application can run them when
they're needed. To do this we could simply change the con gure method, but we're going to
apply a little twist so that every module will be able to register the required migrations.

MODULE AND MODEL INTERFACES


In the last chapter, we registered our routers inside the con guration le. Every module so
far had a router, and it seems like they can have migrations as well. We should realize that
this is a common pattern, and that's why we should create a protocol to unify these things a
bit.

Create a new le inside the App/Framework directory called ModuleInterface.swift.

/// FILE: Sources/App/Framework/ModuleInterface.swift

import Vapor

public protocol ModuleInterface {

static var identifier: String { get }

func boot(_ app: Application) throws


}

public extension ModuleInterface {

func boot(_ app: Application) throws {}

static var identifier: String {


String(describing: self).dropLast(6).lowercased()
}
}

A module is a common interface that can boot required components using the application.
Later on, we'll extend the interface with some other things. The application can iterate
through modules and set up everything using the boot method de ned on the protocol. We
can provide a default implementation for this method to make it optional to implement. We
can also see the identi er property following a standard pattern. We just have to create
every single module with a Module su x, so we can use the type of the module and drop
the last 6 characters, and lowercase the sub-string to come up with a unique value.

Now we can implement a WebModule object le inside the Web directory.

/// FILE: Sources/App/Modules/Web/WebModule.swift

import Vapor

struct WebModule: ModuleInterface {

let router = WebRouter()

func boot(_ app: Application) throws {


try router.boot(routes: app.routes)
}
}

The next one is the BlogModule inside the Blog folder.

60
fi
fi
ffi
fi
fi
fi
fi
fi
/// FILE: Sources/App/Modules/Blog/BlogModule.swift

import Vapor

struct BlogModule: ModuleInterface {

let router = BlogRouter()

func boot(_ app: Application) throws {


app.migrations.add(BlogMigrations.v1())
app.migrations.add(BlogMigrations.seed())

try router.boot(routes: app.routes)


}
}

We have to make sure the schema migration (v1) runs before the database seed migration.
We're going to register this seed after the other migration scripts; you should note that
Fluent migrations are guaranteed to run in the same order as you register them.

For the database models, we can also come up with a generic DatabaseModelInterface.
This will help us to follow a common approach because we'd like to pre x our tables with the
module identi er. A database model in our system will always have a UUID type as a primary
id, so we can make a where constraint based on that to restrict our models. We'd also like to
name our models; this is why we use a common static identi er value and this way we can
automatically give a schema name to our models.

/// FILE: Sources/App/Framework/DatabaseModelInterface.swift

import Vapor
import Fluent

public protocol DatabaseModelInterface: Fluent.Model


where Self.IDValue == UUID
{
associatedtype Module: ModuleInterface

static var identifier: String { get }


}

public extension DatabaseModelInterface {

static var schema: String {


Module.identifier + "_" + identifier
}

static var identifier: String {


String(describing: self)
.dropFirst(Module.identifier.count)
.dropLast(5)
.lowercased() + "s"
}
}

Now we have to update our models to support the newly created database model interface.

/// FILE: Sources/App/Modules/Blog/Database/Models/BlogCategoryModel.swift

import Vapor
import Fluent

final class BlogCategoryModel: DatabaseModelInterface {


typealias Module = BlogModule

static let identifier = "categories"

61
fi
fi
fi
// ...

Since the category word has an irregular plural form, we have to explicitly set the identi er,
but inside our blog post model, we can omit this line because the plural of "post" is "posts"
and that just works for us.

/// FILE: Sources/App/Modules/Blog/Database/Models/BlogPostModel.swift

import Vapor
import Fluent

final class BlogPostModel: DatabaseModelInterface {


typealias Module = BlogModule

// ...

Now we can use the common module interface to register our modules. This will make more
sense instead of registering routes inside the con guration le. Feel free to delete the
original route registration code (Sources/App/routes.swift), since from now on the module
will take care of everything using the boot function that's called from con gure.

/// FILE: Sources/App/con gure.swift

import Vapor
import Fluent
import FluentSQLiteDriver

public func configure(


_ app: Application
) throws {

let dbPath = app.directory.resourcesDirectory + "db.sqlite"


app.databases.use(.sqlite(.file(dbPath)), as: .sqlite)

app.middleware.use(
FileMiddleware(
publicDirectory: app.directory.publicDirectory
)
)

app.middleware.use(ExtendPathMiddleware())

let modules: [ModuleInterface] = [


WebModule(),
BlogModule(),
]
for module in modules {
try module.boot(app)
}
}

We're ready to migrate the database and learn a bit more about Vapor commands.

THE MIGRATION COMMAND


You should know that if you run the application, it'll execute a command called serve by
default, but you don't have to explicitly provide the serve argument. This command is

62
fi
fi
fi
fi
fi
responsible for starting the underlying HTTP server when you run (i.e., vapor run); the serve
command will start listening using the given hostname and port. This is the default
command in Vapor.

To run migrations, you have to start the application using the migrate argument; this will run
the migration scripts instead of starting the webserver.

swift run Run migrate


# or
vapor run migrate

Alternatively you can set command line arguments in Xcode, under the Edit Scheme menu:

If you're using Xcode, you can simply duplicate the scheme and set di erent arguments for
each scheme. This way, you can run migrations by simply selecting e.g. the Migrate scheme
instead of the Run / myProject scheme.

During the very rst migration, Fluent will create an internal table named _ uent_migrations
(yes, the name starts with an underscore). The migration system uses this lookup table to
detect which migrations were already performed and what needs to be done next time you
run the migrate command.

Migration scripts are executed in batches. Each time you run the migrate command, that's
one batch. Every single migration script that needs to run will have the same batch identi er.
Each time you run the migration command, Vapor will check the lookup table, determine
what needs to be done, increase the batch identi er, then run the new migration and save
the migration information inside the table. In this way, if you run the migrate command twice
for the same migration, Vapor can simply ignore it.

63
fi
fi
ff
fl
fi
You can revert the last batch of migrations by running the migrate command with the --revert
ag. This will revert only the last batch of migrations, so you might have to run it multiple
times to revert everything. Alternatively, if you're using SQLite, you can delete the entire
SQLite database le from the disk: this will reset everything. At that point, you have to run
the migrate command to recreate the database's internal structure.

The database le will be created under the working directory. Browsing the SQLite database
le is quite easy: you can download and use the GUI Table Plus application (which costs
$89/yr at the time of this writing), or use the command-line SQLite application that's a part of
the SQLite package, a command-line tool called sqlite3, that's free.

You can download the Table Plus application at https://tableplus.com or install it through the
brew install --cask tableplus command. The sqlite3 application is usually preinstalled on
most operating systems.

USING AUTO-MIGRATION
Each time you run the Vapor application with the migrate argument, at the prompt, you'll
have to con rm the migration. You can skip the con rmation by providing the --auto-migrate
ag as an extra argument when you run the application.

Another option is to call the migration process automatically when the application starts.
We're going to choose this approach because we always want to have the latest version of
our database scheme. You'll see later on that sometimes when you have to start the
application (inside a container) you won't have a chance to run the migration command
upfront.

/// FILE: Sources/App/con gure.swift

import Vapor
import Fluent
import FluentSQLiteDriver

public func configure(


_ app: Application
) throws {

let dbPath = app.directory.resourcesDirectory + "db.sqlite"


app.databases.use(.sqlite(.file(dbPath)), as: .sqlite)

app.middleware.use(
FileMiddleware(
publicDirectory: app.directory.publicDirectory
)
)

app.middleware.use(ExtendPathMiddleware())

let modules: [ModuleInterface] = [


WebModule(),
BlogModule(),
]
for module in modules {
try module.boot(app)
}

try app.autoMigrate().wait()

64
fl
fi
fl
fi
fi
fi
fi
fi
}

The app.autoMigrate method returns a future; this time we want to wait until the migration
process can take care of everything and the database is ready to use. Calling this command
inside the con guration method has the same e ect as running the migrate command with
the --auto-migrate ag using the terminal.

Of course, you should test everything before you deploy the backend server to a production
environment. Don't forget to backup your databases and be ready to restore them if needed.
Accidents can happen and people can make mistakes, but if you know how to solve the
issue when something goes wrong, you can avoid some of those mini heart attacks.

QUERYING MODELS AND DATA TRANSFER OBJECTS (DTOS)


Now that we have a working migration with seed data, we can start using Fluent to retrieve
our models. It would be relatively simple to query all the BlogPostModel objects and replace
the original BlogPost with those types, but I don't like this approach. Why? Well, there are
many reasons, but for example, the model can contain sensitive information: just think about
a user model with a password hash; so it's better to de ne manually an intermediate data
transfer/context object and map the database model into a type.

This is going to be a great way to de ne API objects, and we can also share these types of
Data Transfer Objects (DTOs) with the frontend client: so for example, an iOS application
can reuse this part of the API layer without the need of duplicating code. This will add an
extra layer of security to our system, and in the long term, we can save quite a lot of time.

A Data Transfer Object is, in its simplest form, just a struct with little, if any, functionality of its
own. Say, for example, that you have a model, PersonModel, and you use it to read a Person
record from the database, and now you want to call another function with that Person
record. The Person can be copied to another struct, say PersonContext, which can then be
used to call the next function. We use the terminology Data Transfer Object to refer to that
simple struct.

"Wait a minute," you say, "I can just pass the model because the PersonModel struct is the
same as the proposed PersonContext struct." The response to that is, "Yes, you could do
that, but there are advantages to using the PersonContext to pass the Person to the other
function."

- If we pass PersonModel to our other function, we may also be passing the


capabilities that a PersonModel has: namely, reading and writing the database in this
case. Passing that kind of capability to actors that don't need it represents a security
risk. By copying the Person from the PersonModel to the PersonContext, we create a
structure that doesn't carry additional unnecessary functionality to the next function.
Since the PersonContext will be immutable, it's also safe from accidental modi cation
or modi cation by hacking.

65
fi
fi
fl
fi
ff
fi
fi
- If we're passing a lot of data out of the current scope, the data in a DTO can be
compressed for transmission. This was, in fact, the original idea behind DTOs. Other
people recognized the advantages DTOs have for other applications later.

- Additional context not in the Person record can be added, such as the time it was
retrieved and other related data.

- The DTO can contain limited functionality to convert data formats if needed, and the
compression functions can also be part of the DTO. A simple example of this is, a
UUID? found in the Person record can be converted to a pure String when setting, or
from a String to a UUID when getting.

- When we share a DTO with the frontend of our Blog project instead of a model, we
don't accidentally expose data like passwords or other sensitive information.

Now, getting back to what we were doing, let's refactor the BlogPost struct to represent a
list item for the blog model. We'll move the BlogPost.swift le under the Objects directory.
You can just drag and drop if you're using Xcode, or on the terminal, use:

mv Sources/App/Modules/Blog/BlogPost.swift \
Sources/App/Modules/Blog/Objects/BlogPost.swift

Now, modify it as shown below:

/// FILE: Sources/App/Modules/Blog/Objects/BlogPost.swift

import Foundation

extension Blog.Post {

struct List: Codable {


let id: UUID
let title: String
let slug: String
let image: String
let excerpt: String
let date: Date
}
}

We're going to need a new Swift le called Blog under the Objects folder, and we're going to
use this namespace to place DTOs into. Our very rst DTO is going to be the list object for
the blog post entities. The second one is for the Category namespace.

/// FILE: Sources/App/Modules/Blog/Objects/Blog.swift

enum Blog {

enum Post {

enum Category {

}
}

66
fi
fi
fi
We're also going to need a blog category data transfer list object, so let's make one. Notice
that both the Blog.Post.List and the Blog.Category.List are immutable: an advantage of this
technique.
/// FILE: Sources/App/Modules/Blog/Objects/BlogCategory.swift

import Foundation

extension Blog.Category {

struct List: Codable {


let id: UUID
let title: String
}
}

To render our blog post detail pages, we're also going to need a blog post detail object with
the associated category list object and the contents of the blog post.

/// FILE: Sources/App/Modules/Blog/Objects/BlogPost.swift

import Foundation

extension Blog.Post {

struct List: Codable {


let id: UUID
let title: String
let slug: String
let image: String
let excerpt: String
let date: Date
}

struct Detail: Codable {


let id: UUID
let title: String
let slug: String
let image: String
let excerpt: String
let date: Date
let category: Blog.Category.List
let content: String
}
}

If you think about it, this is how an actual RESTful JSON API interface looks. Well, the good
news is that later on, we're going to reuse these objects to create an API for our application.
Right now we can use the DTOs to render our HTML templates safely. First, we have to alter
our BlogPostsContext.

/// FILE: Sources/App/Modules/Blog/Templates/Contexts/BlogPostsContext.swift

struct BlogPostsContext {
let icon: String
let title: String
let message: String
let posts: [Blog.Post.List]
}

The next one is the BlogPost context, we're going to use the Blog.Post.Detail object there.

/// FILE: Sources/App/Modules/Blog/Templates/Contexts/BlogPostContext.swift

67
struct BlogPostContext {
let post: Blog.Post.Detail
}

Now let's focus on the blog frontend controller; I'll show you how to use Fluent to query all of
the posts. Feel free to delete the original posts variable and change the following methods.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogFrontendController.swift

import Vapor
import Fluent

struct BlogFrontendController {

func blogView(req: Request) async throws -> Response {


let postModels = try await BlogPostModel
.query(on: req.db)
.sort(\.$date, .descending)
.all()

let posts = try postModels.map {


Blog.Post.List(
id: try $0.requireID(),
title: $0.title,
slug: $0.slug,
image: $0.imageKey,
excerpt: $0.excerpt,
date: $0.date
)
}

let ctx = BlogPostsContext(


icon: "🔥 ",
title: "Blog",
message: "Hot news and stories about everything.",
posts: posts)

return req.templates.renderHtml(
BlogPostsTemplate(ctx)
)
}

func postView(req: Request) async throws -> Response {


let slug = req.url.path.trimmingCharacters(
in: .init(charactersIn: "/")
)
guard
let post = try await BlogPostModel
.query(on: req.db)
.filter(\.$slug == slug)
.with(\.$category)
.first()
else {
return req.redirect(to: "/")
}
let ctx = BlogPostContext(
post: Blog.Post.Detail(
id: post.id!,
title: post.title,
slug: post.slug,
image: post.imageKey,
excerpt: post.excerpt,
date: post.date,
category: .init(
id: post.category.id!,
title: post.category.title
),
content: post.content
)
)

68
return req.templates.renderHtml(
BlogPostTemplate(ctx)
)
}
}

We use the static query method on the Model to request entities from the database table.
This returns a query builder instance that you can tweak by adding various lter, limit, and
sort options. Using the with method you can load relationship objects into the model. The all
method will execute the query and return the requested rows as Model objects. We'll see
more examples of database queries later on.

Since we're using async functions to fetch blog posts from the database, we also had to add
the async keyword to the function signature for both request handler methods. Fortunately,
Vapor can register sync and async request handlers together, so it's not a big deal, but if you
don't put the async keyword you won't be able to call async methods inside the function, so
the compiler will protect you from yourself.

You might say that it's quite ugly that you have to write these map functions, and I totally
agree, but please don't worry about it just yet; we're going to nd a new place for these map
functions in a later chapter.

The last thing we can do is get rid of folders we'll no longer need. You can execute these
commands in the terminal:

cd ~/myProject
rm -Rf Sources/App/Controllers
rm -Rf Sources/App/Migrations
rm -Rf Sources/App/Models

This should remove the Controllers, Migrations and Models folders from the App directory.

SUMMARY
In this chapter we've explored how Fluent works and we've successfully migrated our blog
storage to a working SQLite database. We've learned about schema builders and how they
can help us to create type-safe SQL database tables for our models. We've also learned a lot
about modeling database entities using various eld types and relations through property
wrappers. In addition to our current modular structure, we've introduced a module and a
database model protocol, and we've learned why it's good to have separated template
context or data transfer objects to render Fluent entities.

69
fi
fi
fi
CHAPTER 5:
SESSIONS AND USER AUTHENTICATION

In this chapter, we're going to focus on building a session-based web authentication layer.
Users will be able to sign in using a form, and already logged in users will be detected with
the help of a session cookie and persistent session storage using Fluent. In the second half
of this chapter, I'll show you how to create custom authenticator middlewares; that'll allow
you to authenticate users based on sessions or credentials

THE USER MODULE


You should have a basic knowledge of HTML by now because we've already used a
considerable amount of it in previous chapters. You should understand how to set up web
pages to display information (GET), request input (GET with FORM), and submit user data
(FORM with POST). If you're not familiar with basic HTML, this would be a good time to
improve your understanding by reviewing it on a website like W^3 Schools HTML Tutorial.

Just to review, the GET method is the method used by the browser to ask the server to send
back a given resource without changing it. A request from the browser to retrieve a resource
is done through the URL (address bar). Its attribute is .method(.get).

The POST method is the method used by the browser to ask the server to change a given
resource (usually), but it doesn't have to be a change request: it can be used to request a
resource like a GET, but such use would be considered unconventional by most
programmers. A request from the browser to change a resource is done through the request
body. Its attribute is .method(.post).

We're going to use a brand new DSL library here called SwiftHtml that we also already
learned in a previous chapter, but if you want to review it, check out the article on SwiftHtml
on theThe.Swift.Dev. Website. You can nd lots of helpful articles there also.

Let’s start by creating the new folders we’ll use during this chapter. You can use the
commands below to add them:

cd ~/myProject
mkdir -p Sources/App/Modules/User
mkdir -p Sources/App/Modules/User/Database
mkdir -p Sources/App/Modules/User/Database/Models
mkdir -p Sources/App/Modules/User/Database/Migrations
mkdir -p Sources/App/Modules/User/Templates
mkdir -p Sources/App/Modules/User/Templates/Contexts
mkdir -p Sources/App/Modules/User/Templates/Html
mkdir -p Sources/App/Modules/User/Controllers
mkdir -p Sources/App/Modules/User/Authenticators

70
fi
The User module is going to be responsible for user management and authentication. If we
want to be able to log in with a given email and password combination, we'll have to create a
model for the user account objects. We'll work with the following UserAccountModel entity.

If we want to be able to log in with a given email and password combination we'll have to
create a model for the user account objects. We'll work with the following
UserAccountModel entity.

/// FILE: Sources/App/Modules/User/Database/Models/UserAccountModel.swift

import Vapor
import Fluent

final class UserAccountModel: DatabaseModelInterface {


typealias Module = UserModule

struct FieldKeys {
struct v1 {
static var email: FieldKey { "email" }
static var password: FieldKey { "password" }
}
}

@ID()
var id: UUID?

@Field(key: FieldKeys.v1.email)
var email: String

@Field(key: FieldKeys.v1.password)
var password: String

init() { }

init(
id: UUID? = nil,
email: String,
password: String
) {
self.id = id
self.email = email
self.password = password
}
}

Before we can use the model, we need the corresponding migration to create the table for
the users. We're going to put a unique constraint on the email eld because any given
person (as represented by an email address) should only have (and only needs) one login.
All the passwords are going to be stored as encrypted strings. Never store plain text
passwords in your database, and always check which is the best encryption algorithm (at the
time of this writing, BCrypt is rated as the most secure). We're also going to set up a seed
where we create a root user account.

/// FILE: Sources/App/Modules/User/Database/Migrations/UserMigrations.swift

import Vapor
import Fluent

enum UserMigrations {

struct v1: AsyncMigration {

func prepare(on db: Database) async throws {


try await db.schema(UserAccountModel.schema)

71
fi
.id()
.field(UserAccountModel.FieldKeys.v1.email, .string, .required)
.field(UserAccountModel.FieldKeys.v1.password, .string, .required)
.unique(on: UserAccountModel.FieldKeys.v1.email)
.create()
}

func revert(on db: Database) async throws {


try await db.schema(UserAccountModel.schema).delete()
}
}

struct seed: AsyncMigration {

func prepare(on db: Database) async throws {


let email = "[email protected]"
let password = "ChangeMe1"
let user = UserAccountModel(
email: email,
password: try Bcrypt.hash(password)
)
try await user.create(on: db)
}

func revert(on db: Database) async throws {


try await UserAccountModel.query(on: db).delete()
}
}
}

Under macOS 10.15, please note that there's a known bug with bcrypt, when you use the
command line to run your migrations (with swift run migrate or vapor run migrate): the app
won't be able to perform the database migration; instead, it'll crash with a Segmentation
fault: 11 error. The migration works with Xcode, so if you're using Xcode you should add the
migrate parameter under the scheme con guration. If you're running a newer version of
macOS, you should be ne.

Note: SHA0, SHA1, SHA2, and SHA3 are all fast hashes that are bad for passwords. bcrypt
is a slow hash that's good for passwords (precisely because it's slow). Always use a slow
hash, never a fast hash for passwords. It's also worth noting that while some authors point
out how vulnerable passwords are, breaking a password by brute force requires that the
hacker has already stolen the database with the hashes in it. Without the hash, there's
nothing for the hacker to compare, and no way to know if the code he's testing is the actual
password. The only other way to know is by trying each code in the real website, but if a
slow hash is used, it'll take prohibitively long; also, too many tries on a well-designed
website will trigger a lock-out of the hacker.

The takeaway from this is that the security of the password table containing the hash codes
must be paramount. You also may want to consider keeping your password table separate
from your user demographics table.

As the last step, we still have to create a UserModule, and we have to register our
migrations.

/// FILE: Sources/App/Modules/User/UserModule.swift

import Vapor

struct UserModule: ModuleInterface {

72
fi
fi
func boot(_ app: Application) throws {
app.migrations.add(UserMigrations.v1())
app.migrations.add(UserMigrations.seed())
}
}

Don't forget to alter the con guration le: we're going to init the User module and boot it.
Modify the last part of the con guration to look like the code below:

/// FILE: Sources/App/con gure.swift

import Vapor
import Fluent
import FluentSQLiteDriver

public func configure(


_ app: Application
) throws {

// ...

let modules: [ModuleInterface] = [


WebModule(),
UserModule(),
BlogModule(),
]
for module in modules {
try module.boot(app)
}

try app.autoMigrate().wait()
}

Now, if you run the app, the new User account table will be migrated and will include the
default root user.

SIGNING IN
Sessions are entities stored somewhere on the backend. The storage can be in-memory
storage, a Fluent database table, or a standalone Redis server. Memory storage is the
default session storage driver, but it can be problematic. When you restart the server, all your
sessions will be gone; the same thing happens if the server crashes.

We could use a Redis server, but that would require some additional setup. Since we aren't
expecting a heavy load on our blog, we'll be just ne with a regular database storage. It's
possible to change the default session storage from a memory-based session driver to one
using Fluent by using the .use method to specify Fluent.

/// FILE: Sources/App/con gure.swift

import Vapor
import Fluent
import FluentSQLiteDriver

public func configure(


_ app: Application
) throws {

73
fi
fi
fi
fi
fi
fi
// ...

app.sessions.use(.fluent)
app.migrations.add(SessionRecord.migration)
app.middleware.use(app.sessions.middleware)

let modules: [ModuleInterface] = [


WebModule(),
UserModule(),
BlogModule(),
]
for module in modules {
try module.boot(app)
}

try app.autoMigrate().wait()
}

The very rst line tells the Vapor framework that it should use the Fluent session driver; that's
essentially just a table in the default database storage. The second line adds a new
migration for this underlying _ uent_sessions table. Maybe in a future release, this will be
done automatically; that would be the ideal scenario, but for now you have to do it.

The last line of the con guration registers the SessionsMiddleware using the system driver
and con guration. This middleware will try to load the session data from the session cookie
stored locally on the client-side. The session cookie simply stores a session identi er on the
client-side without additional data. Everything related to the session is stored inside the
database table located on the server. We've nished with the preparation for now, and we'll
talk a bit more about sessions later on in this chapter when we start talking about
authenticators.

So how do we log in through a website? We need to present a login form to the user that will
return the user's credentials to our application for authentication. Let's make one real quick;
we can place this new UserLoginTemplate inside the User/Templates/Html folder and we'll
also need a UserLoginContext inside the User/Templates/Contexts directory.

/// FILE: Sources/App/Modules/User/Templates/Contexts/UserLoginContext.swift

struct UserLoginContext {

let icon: String


let title: String
let message: String
let email: String?
let password: String?
let error: String?

init(
icon: String,
title: String,
message: String,
email: String? = nil,
password: String? = nil,
error: String? = nil
) {
self.icon = icon
self.title = title
self.message = message
self.email = email
self.password = password
self.error = error
}
}

74
fi
fi
fi
fl
fi
fi
Building up a form is simple: we can just use some basic HTML tag elements. The Form
element has an action and a method modi er. The action is the destination URL (the POST /
sign-in/ endpoint) to which the submission event will send the data. The method can have
one of two values: .get or .post. To display form data, GET is used; to return user input data,
POST is used. In this form we want to collect and return user credentials, so we need POST.

We only need two input elds, one for the email and one for a password, plus a submit
button to post our data. We can con gure the form elds by using the Input tag. This is
relatively simple if you know HTML: you can use the modi ers to set up the underlying
attribute values. One extra addition is the key modi er (that's not an HTML tag, but a
SwiftHtml shortcut), which will set both the id and the name attributes at the same time.

/// FILE: Sources/App/Modules/User/Templates/Contexts/UserLoginTemplate.swift

import Vapor
import SwiftHtml

struct UserLoginTemplate: TemplateRepresentable {

var context: UserLoginContext

init(
_ context: UserLoginContext
) {
self.context = context
}

@TagBuilder
func render(
_ req: Request
) -> Tag {
WebIndexTemplate(
.init(title: context.title)
) {
Div {
Section {
P(context.icon)
H1(context.title)
P(context.message)
}
.class("lead")

Form {
if let error = context.error {
Section {
Span(error)
.class("error")
}
}
Section {
Label("Email:")
.for("email")
Input()
.key("email")
.type(.email)
.value(context.email)
.class("field")
}
Section {
Label("Password:")
.for("password")
Input()
.key("password")
.type(.password)
.value(context.password)
.class("field")
}
Section {

75
fi
fi
fi
fi
fi
fi
Input()
.type(.submit)
.value("Sign in")
.class("submit")
}
}
.action("/sign-in/")
.method(.post)
}
.id("user-login")
.class("container")
}
.render(req)
}
}

This is going to be a user-facing frontend login form, so we have to render the index
template.

If we render this template and press the submit button now, the browser will perform a POST
HTTP request to the /sign-in/ endpoint with the URLEncoded contents of the form elds. We
need two endpoints to make everything work. One endpoint is going to be responsible for
the form rendering, and the other will handle the data that will be submitted through the
POST request. I prefer to name these functions with the View and Action su xes (I.e.,
signInView and signInAction). Following this naming convention makes it easier to see that
these two are related.

Note: The routes are going to be GET /sign-in/ and POST /sign-in/ and those two end-
points call signInView and signInAction respectively. Also, it's important to recognize that
the submitted data is URLEncoded because it determines what Vapor decoder will be used
by the receiving endpoint.

Let's prepare a dummy UserFrontendController with these methods. We'll build on the todo
part later, but for now, we put this reminder that there's something we have to come back to.

/// FILE: Sources/App/Modules/User/Controllers/UserFrontendController.swift

import Vapor

struct UserFrontendController {

func signInView(_ req: Request) async throws -> Response {


let template = UserLoginTemplate(
.init(
icon: "⬇ ",
title: "Sign in",
message: "Please log in with your existing account"
)
)
return req.templates.renderHtml(template)
}

func signInAction(_ req: Request) async throws -> Response {


// @TODO: handle sign in action
try await signInView(req)
}
}

We can register these endpoints through a UserRouter object.

76
ffi
fi
/// FILE: Sources/App/Modules/User/UserRouter.swift

import Vapor

struct UserRouter: RouteCollection {

let frontendController = UserFrontendController()

func boot(
routes: RoutesBuilder
) throws {
routes.get("sign-in", use: frontendController.signInView)
routes.post("sign-in", use: frontendController.signInAction)
}
}

Inside our user module, we still have to call the boot method to make these routes work.

/// FILE: Sources/App/Modules/User/UserModule.swift

import Vapor

struct UserModule: ModuleInterface {

let router = UserRouter()

func boot(_ app: Application) throws {


app.migrations.add(UserMigrations.v1())
app.migrations.add(UserMigrations.seed())

try router.boot(routes: app.routes)


}
}

Now, if we visit the /sign-in/ endpoint, we should see a simple login form, but don't expect
too much because we don't handle the login action properly. That's going to be the next step
where we introduce authenticators.

AUTHENTICATORS
An authenticator is a middleware that will try to sign in with an authenticatable object, if the
necessary data exists in the request. The authentication data is stored in the req.auth
property, and there's a login and a logout method de ned on the request authentication
object to help you code the login and logout endpoints.

You should note that the req.auth variable isn't the same as the req.session property. They
serve quite di erent purposes. However, you can store SessionAuthenticatable objects
inside the req.session variable. These objects will be persisted, and on the client-side, a
session cookie will be used to track the current session. This allows us to keep users signed
in after they're properly authenticated through the login form.

We could use the UserModel and conform to the Authenticatable protocol, but since that
object contains some sensitive data, I prefer to use a separate AuthenticatedUser struct to
store user data inside the req.auth property. The AuthenticatedUser struct is going to be
publicly available since it's going to be part of our framework. The SessionAuthenticatable

77
ff
fi
protocol extends the Authenticatable interface, so it makes sense that it should conform to
the later one.

/// FILE: Sources/App/Framework/AuthenticatedUser.swift

import Vapor

public struct AuthenticatedUser {

public let id: UUID


public let email: String

public init(
id: UUID,
email: String
) {
self.id = id
self.email = email
}
}

extension AuthenticatedUser: SessionAuthenticatable {


public var sessionID: UUID { id }
}

When a user has to provide a correct email and password combination, we call that a
credential-based authentication. We can use these values to perform a lookup inside the
accounts table to check if it's an existing record or not and see if the elds match. If
everything is correct, we can authenticate the user (meaning the login attempt was
successful). We're going to write a standalone CredentialsAuthenticator that can be used to
perform this action.
/// FILE: Sources/App/Modules/User/Authenticators/UserCredentialsAuthenticator.swift

import Vapor
import Fluent

struct UserCredentialsAuthenticator: AsyncCredentialsAuthenticator {

struct Credentials: Content {


let email: String
let password: String
}

func authenticate(
credentials: Credentials,
for req: Request
) async throws {
guard
let user = try await UserAccountModel
.query(on: req.db)
.filter(\.$email == credentials.email)
.first()
else {
return
}
do {
guard try Bcrypt.verify(
credentials.password,
created: user.password
) else {
return
}
req.auth.login(
AuthenticatedUser(
id: user.id!,
email: user.email

78
fi
)
)
}
catch {
// do nothing...
}
}
}

Note: The input is a Content object which is Vapor's de nition for something that can be
decoded from an incoming request or encoded as a response. Vapor has multiple types of
content that are all either JSON or URLEncoded content with their corresponding encoders
and decoders. You remember that the HTML form is sending URLEncoded content when the
user presses the submit button.

The authenticate function receives the credentials and tries to look for an existing user with
a valid corresponding password in the database. If a record is found, we can call the
req.auth.login method with the previously created AuthenticatedUser object. This will save
our user's info into the auth storage after which the other request handlers can simply check
to see if there's an existing AuthenticatedUser or not; this will indicate if the user is logged
in.

We're going to use this authenticator for our POST /sign-in/ route. We can group routes by
paths or middlewares, and since authenticators are derived from middlewares, it's possible
to use them as a group value.

/// FILE: Sources/App/Modules/User/UserRouter.swift

import Vapor

struct UserRouter: RouteCollection {

let frontendController = UserFrontendController()

func boot(
routes: RoutesBuilder
) throws {
routes.get("sign-in", use: frontendController.signInView)

routes
.grouped(
UserCredentialsAuthenticator()
)
.post("sign-in", use: frontendController.signInAction)
}
}

We should also update the user frontend controller to implement our signInAction method
where we left a stub.

/// FILE: Sources/App/Modules/User/Controllers/UserFrontendController.swift

import Vapor

struct UserFrontendController {

private struct Input: Decodable {


let email: String?
let password: String?
}

79
fi
private func renderSignInView(
_ req: Request,
_ input: Input? = nil,
_ error: String? = nil
) -> Response {

let template = UserLoginTemplate(


.init(
icon: "⬇ ",
title: "Sign in",
message: "Please log in with your existing account",
email: input?.email,
password: input?.password,
error: error
)
)
return req.templates.renderHtml(template)
}

func signInView(_ req: Request) async throws -> Response {


renderSignInView(req)
}

func signInAction(_ req: Request) async throws -> Response {


/// if the user is authenticated, we can store the user data inside the session too
if let user = req.auth.get(AuthenticatedUser.self) {
req.session.authenticate(user)
return req.redirect(to: "/")
}
/// if the user credentials were wrong we render the form again with an error message
let input = try req.content.decode(Input.self)
return renderSignInView(
req,
input,
"Invalid email or password."
)
}
}

We've created some private helpers: the Input is a Decodable struct that we can use to pass
back the submitted values when we render the login form; The renderSignInView will help
us to simplify the form rendering process because we want to render the form both in the
signInView and the signInAction method, but we use di erent arguments.

Understanding the call order inside the sign-in action method is very important. First we call
the credentials authenticator, req.auth.get; if the user credentials were OK, we store the
authenticated user in the session; and if the credentials weren't validated, we redisplay the
sign-in form with an error message.

Now that we can authenticate a user through the login form and save it to the session
storage, we need a method to retrieve the user from the session storage. Remember, the
session storage is the one that belongs to the client (browser) that's communicating with the
application at this moment. Using this, we'll be able to determine if the session's user is
logged in. If so, we can display some user-related data on the web frontend.

The SessionAuthenticator can check the value of a session cookie and authenticate a user
based on that identi er. Cookies are transferred using HTTP headers; the authenticator
protocol is automatically parsing the session identi er from the request for you. The HTTP
protocol is stateless by default and the session storage is designed to carry over state
information through various pages or requests using cookies.

80
fi
fi
ff
The UserSessionAuthenticator should check the database to see if there's a valid user
associated with a given SessionID and log in the returned user if there was one. We can put
this new authenticator into the User/Authenticators folder we created at the beginning of
the chapter, just like we did before with the UserCredentialsAuthenticator.

/// FILE: Sources/App/Modules/User/Authenticators/UserSessionAuthenticator.swift

import Vapor
import Fluent

struct UserSessionAuthenticator: AsyncSessionAuthenticator {


typealias User = AuthenticatedUser

func authenticate(
sessionID: User.SessionID,
for req: Request
) async throws {
guard
let user = try await UserAccountModel
.find(sessionID, on: req.db)
else {
return
}
req.auth.login(
AuthenticatedUser(
id: user.id!,
email: user.email
)
)
}
}

Now that we have a session authenticator, we should use it. For the sake of simplicity, we're
going to add this as a global middleware, so it'll be called before every single route handler
that we register.

/// FILE: Sources/App/Modules/User/UserModule.swift

import Vapor

struct UserModule: ModuleInterface {

let router = UserRouter()

func boot(_ app: Application) throws {


app.migrations.add(UserMigrations.v1())
app.migrations.add(UserMigrations.seed())

app.middleware.use(UserSessionAuthenticator())

try router.boot(routes: app.routes)


}
}

Hopefully, if we did everything right, we should be able to update the index template and
check to see if there's a logged-in user, or if we have to perform a login action. This can be
done through the req.auth property.

Note: the Div below is the one that has the .class("menu-items") modi er. This code will
extend that one with the Sign in and Sign out menu items.

/// FILE: Sources/App/Modules/Web/Templates/Html/WebIndexTemplate.swift

// ...

81
fi
Div {
A("Home")
.href("/")
.class("selected", req.url.path == "/")
A("Blog")
.href("/blog/")
.class("selected", req.url.path == "/blog/")
A("About")
.href("#")
.onClick("javascript:about();")

if req.auth.has(AuthenticatedUser.self) {
A("Sign out")
.href("/sign-out/")
}
else {
A("Sign in")
.href("/sign-in/")
}
}
.class("menu-items")
}
// ...

Implementing the sign-out endpoint is trivial: we just have to logout and unauthenticate the
AuthenticatedUser, and that also removes it from the session storage. Finally, we can simply
redirect back to the home page after a successful logout action.

/// FILE: Sources/App/Modules/User/Controllers/UserFrontendController.swift

import Vapor

struct UserFrontendController {

// ...
func signOut(req: Request) throws -> Response {
req.auth.logout(AuthenticatedUser.self)
req.session.unauthenticate(AuthenticatedUser.self)
// req.session.destroy()
return req.redirect(to: "/")
}
}

Don't forget to register this signOut method inside the UserRouter.

/// FILE: Sources/App/Modules/User/UserRouter.swift

import Vapor

struct UserRouter: RouteCollection {

let frontendController = UserFrontendController()

func boot(
routes: RoutesBuilder
) throws {
routes.get("sign-in", use: frontendController.signInView)

routes
.grouped(
UserCredentialsAuthenticator()
)
.post("sign-in", use: frontendController.signInAction)

routes.get("sign-out", use: frontendController.signOut)


}
}

82
We could also check in the signInView method to see if there's a logged in user and redirect
the browser to another endpoint, or even get the user data and render a di erent view; I'm
not going to add this logic here, but I'll show you how to do that in a di erent controller.

Start the server and try to log in with the pre-created user account. You can also bring up the
inspector and take a look at the local cookie storage. The session identi er should change
every time you perform a log-in or log-out action if you call the session.destroy method,
otherwise you can keep the same session id in between account changes.

SUMMARY
In this chapter, we've created a brand new user module with the corresponding database
models and migrations. We've also introduced the concept of authenticators, and we've
learned how to authenticate a generic user object. We've talked about the di erences
between the session and the auth objects and nally, we've managed to build a sign-in
mechanism.

83
fi
ff
fi
ff
ff
CHAPTER 6:
ABSTRACT FORMS AND FORM FIELDS

This chapter is all about creating an abstract form builder that we can use to generate HTML
forms. We're going to de ne reusable form elds with corresponding context objects using a
model view view-model-like architecture. This will allow us to compose all kinds of input
forms by reusing generic elds. In the second half of the chapter, we're going to talk about
processing user input, and loading and persisting data using a protocol-oriented solution.
Finally, we're going to rebuild our already existing user login form by using the components.

REUSABLE FORM FIELDS


Let’s start by creating the new folders we’ll use during this chapter. You can use the
commands below to add them:

cd ~/myProject
mkdir -p Sources/App/Framework/Form
mkdir -p Sources/App/Framework/Form/Templates
mkdir -p Sources/App/Framework/Form/Templates/Contexts
mkdir -p Sources/App/Framework/Form/Templates/Html
mkdir -p Sources/App/Framework/Form/Fields
mkdir -p Sources/App/Modules/User/Forms

As a very rst step, we should create a new Form directory inside the framework folder
where we can put all the shared form components. We can start with a LabelContext object,
which is going to represent a label for a given form eld. Place it inside a Templates/
Contexts sub-directory.

First, let's consider reusable objects to help build the input page (GET).

/// FILE: Sources/App/Framework/Form/Templates/Contexts/LabelContext.swift

public struct LabelContext {

public var key: String


public var title: String?
public var required: Bool
public var more: String?

public init(
key: String,
title: String? = nil,
required: Bool = false,
more: String? = nil
) {
self.key = key
self.title = title
self.required = required
self.more = more
}
}

84
fi
fi
fi
fi
fi
As we've learned from the previous chapters, we're going to need a template le to render a
context object. In this case, the key property will be used for the input eld identi cation; the
title will be used to print out the actual label; the required ag will mark a required input eld
with an asterisk (*) character and the more value will be used to display additional info.

/// FILE: Sources/App/Framework/Form/Templates/Html/LabelTemplate.swift

import Vapor
import SwiftHtml

public struct LabelTemplate: TemplateRepresentable {

var context: LabelContext

public init(
_ context: LabelContext
) {
self.context = context
}

@TagBuilder
public func render(
_ req: Request
) -> Tag {
Label {
Text(context.title ?? context.key.capitalized)

if let more = context.more {


Span(more)
.class("more")
}
if context.required {
Span("*")
.class("required")
}
}.for(context.key)
}
}

LabelTemplate is a reusable label that we can use to render a form eld object.

If we want to refactor the user login form, we're also going to require a generic input eld
that can render an email and a password-styled HTML input. We're going to use an
InputFieldContext struct to set up the input template. We're going to put all the eld-related
les into the Form/Fields directory.

/// FILE: Sources/App/Framework/Form/Fields/InputFieldContext.swift

import SwiftHtml

public struct InputFieldContext {

public let key: String


public var label: LabelContext
public var type: SwiftHtml.Input.`Type`
public var placeholder: String?
public var value: String?
public var error: String?

public init(
key: String,
label: LabelContext? = nil,
type: Input.`Type` = .text,
placeholder: String? = nil,

85
fi
fl
fi
fi
fi
fi
fi
fi
fi
value: String? = nil,
error: String? = nil
) {
self.key = key
self.label = label ?? .init(key: key)
self.type = type
self.placeholder = placeholder
self.value = value
self.error = error
}
}

The type enum is going to be used to set the type of the input eld and the key is going to
be a unique value for each eld; later on, we'll be able to use this key to retrieve the eld
values on the server-side. The value is going to be a simple String value, and if it's left empty,
it'll be lled with placeholder instead. The label property is a LabelContext that's going to be
used to render the label template. The error property is an optional value, if there was an
error (not nil), we're going to display it.

/// FILE: Sources/App/Framework/Form/Fields/InputFieldTemplate.swift

import Vapor
import SwiftHtml

public struct InputFieldTemplate: TemplateRepresentable {

public var context: InputFieldContext

public init(
_ context: InputFieldContext
) {
self.context = context
}

@TagBuilder
public func render(
_ req: Request
) -> Tag {
LabelTemplate(context.label).render(req)

Input()
.type(context.type)
.key(context.key)
.placeholder(context.placeholder)
.value(context.value)
.class("field")
if let error = context.error {
Span(error)
.class("error")
}
}
}

Second, let's talk about how to receive the POST data. It would be nice to have something to
combine the input and the output templates. Ideally, we'd like to be able to process forms by
decoding a generic input value, and later on, when we have to render the form, it'd be nice
to use a generic template to display the actual form eld. We're going to create an abstract
form eld class to make this happen.

/// FILE: Sources/App/Framework/Form/AbstractFormField.swift

import Vapor

open class AbstractFormField<


Input: Decodable,
Output: TemplateRepresentable

86
fi
fi
fi
fi
fi
fi
> {

public var key: String


public var input: Input
public var output: Output
public var error: String?

public init(
key: String,
input: Input,
output: Output,
error: String? = nil
) {
self.key = key
self.input = input
self.output = output
self.error = error
}

open func config(


_ block: (AbstractFormField<Input, Output>) -> Void
) -> Self {
block(self)
return self
}
}

The key is going to be used to decode the input and set the key of the output template. The
input and the output values are generic values, they're going to be speci ed in a subclass.
We're also going to add an error property to the AbstractFormField class, so we can
validate the input and render the problems later on if there were any.

This is how we can de ne the actual InputField based on the existing components.

/// FILE: Sources/App/Framework/Form/Fields/InputField.swift

public final class InputField: AbstractFormField<


String,
InputFieldTemplate
> {

public convenience init(_ key: String) {


self.init(
key: key,
input: "",
output: .init(
.init(
key: key
)
)
)
}
}

We still have to store the elds somehow inside a form class, but before we do that, we also
have to con gure the form action. This involves a submission url, a method and an enctype
value.

/// FILE: Sources/App/Framework/Form/FormAction.swift

import SwiftHtml

public struct FormAction {

public var method: SwiftHtml.Method


public var url: String?

87
fi
fi
fi
fi
public var enctype: SwiftHtml.Enctype?

public init(
method: SwiftHtml.Method = .post,
url: String? = nil,
enctype: SwiftHtml.Enctype? = nil
) {
self.method = method
self.url = url
self.enctype = enctype
}
}

That's it: we can now de ne an AbstractForm class that can store all the available form elds
plus the form action. A form can also have a generic error message, so we store an error
property for this purpose. For example, if a login attempt fails, we just want to print the
"Invalid email or password" message instead of speci c form eld errors, but if an email input
was messed up, we might want to display an "Invalid email address" message next to that
eld.

/// FILE: Sources/App/Framework/Form/AbstractForm.swift

import Vapor

open class AbstractForm {

open var action: FormAction


open var fields: [Any]
open var error: String?
open var submit: String?

public init(
action: FormAction = .init(),
fields: [Any] = [],
error: String? = nil,
submit: String? = nil
) {
self.action = action
self.fields = fields
self.error = error
self.submit = submit

self.action.enctype = .multipart
}
}

We still have one big problem: Swift won't allow us to store an [AbstractFormField<Input,
Output>] array because it's a generic class with unknown Input and Output values. For now,
we're going to put an Any placeholder into the elds array.

Anyway, we still need a way to render a form, so we're going to create a FormContext. The
elds are going to be represented as TemplateRepresentable values.

/// FILE: Sources/App/Framework/Form/Templates/Contexts/FormContext.swift

public struct FormContext {


public var action: FormAction
public var fields: [TemplateRepresentable]
public var error: String?
public var submit: String?
}

We can use the context to render the form with the elds inside the FormTemplate.

88
fi
fi
fi
fi
fi
fi
fi
fi
/// FILE: Sources/App/Framework/Form/Templates/Html/FormTemplate.swift

import Vapor
import SwiftHtml

public struct FormTemplate: TemplateRepresentable {

var context: FormContext

public init(
_ context: FormContext
) {
self.context = context
}

@TagBuilder
public func render(
_ req: Request
) -> Tag {
Form {
if let error = context.error {
Section {
P(error)
.class("error")
}
}
for field in context.fields {
Section {
field.render(req)
}
}

Section {
Input()
.type(.submit)
.value(context.submit ?? "Save")
}
}
.method(context.action.method)
.action(context.action.url)
.enctype(context.action.enctype)
}
}

The logic is pretty simple: we just have to iterate through the form elds and call the render
method on the TemplateRepresentable protocol. We display the error if the context has a
non-nil value, and we also set the proper action, method, and enctype values.

That's our generic form setup; we almost have everything that we need to use forms, but we
still have to handle various events and we have to x the Any type issue inside the
AbstractForm class. In the next part of the chapter, we're going to focus on these things.

FORM COMPONENTS
A form component is something that can respond to an event that happens with a form.
There are going to be multiple events that we'd like to handle.

A load event happens when we try to initially load the form; this is a great way to load
related models from the database that are required to render the form: for example, a list of
option values that are stored as a di erent entity. Before the render method, the read

89
ff
fi
fi
method should be called; we can use that method to read back the actual values of the
elds. The render method will be called when the backend tries to display the form using the
template engine.

The process method is responsible for processing the input data that the user submits
through the form. After the data is stored inside the input value of the form eld, the validate
method should be called so that validations can be evaluated and errors can be set. The
write method is a great way to set valid input values using a model. The save method can be
used to perform additional save operations after we've written back the validated data
values.

Based on the requirements, this is how the FormComponent protocol should look.

/// FILE: Sources/App/Framework/Form/FormComponent.swift

import Vapor

public protocol FormComponent {

func load(req: Request) async throws


func process(req: Request) async throws
func validate(req: Request) async throws -> Bool
func write(req: Request) async throws
func save(req: Request) async throws
func read(req: Request) async throws
func render(req: Request) -> TemplateRepresentable
}

A form component is going to represent the form elds array in our AbstractFormField
class. First, we have to implement the required methods inside the AbstractFormField class.

/// FILE: Sources/App/Framework/Form/AbstractFormField.swift

import Vapor

open class AbstractFormField<


Input: Decodable,
Output: TemplateRepresentable
>: FormComponent {

public var key: String


public var input: Input
public var output: Output
public var error: String?

public init(
key: String,
input: Input,
output: Output,
error: String? = nil
) {
self.key = key
self.input = input
self.output = output
self.error = error
}

open func config(


_ block: (AbstractFormField<Input, Output>) -> Void
) -> Self {
block(self)
return self
}

open func load(req: Vapor.Request) async throws {

90
fi
fi
fi
}

open func process(req: Vapor.Request) async throws {


if let value = try? req.content.get(Input.self, at: key) {
input = value
}
}

open func validate(req: Vapor.Request) async throws -> Bool {


true
}

open func write(req: Vapor.Request) async throws {

open func save(req: Vapor.Request) async throws {

open func read(req: Vapor.Request) async throws {

open func render(req: Vapor.Request) -> TemplateRepresentable {


output
}
}

We don't de ne these methods in an extension because we'd like to allow child classes to
override them if needed. So far the current version will just work, we're going to ll in the
missing gaps later on. Let's move on to the abstract form class and extend it with the
FormComponent protocol too.

/// FILE: Sources/App/Framework/Form/AbstractForm.swift

import Vapor

open class AbstractForm: FormComponent {

open var action: FormAction


open var fields: [FormComponent]
open var error: String?
open var submit: String?

public init(
action: FormAction = .init(),
fields: [FormComponent] = [],
error: String? = nil,
submit: String? = nil
) {
self.action = action
self.fields = fields
self.error = error
self.submit = submit

self.action.enctype = .multipart
}

open func load(req: Request) async throws {


for field in fields {
try await field.load(req: req)
}
}

open func process(req: Request) async throws {


for field in fields {
try await field.process(req: req)
}

91
fi
fi
}

open func validate(req: Request) async throws -> Bool {


var result: [Bool] = []
for field in fields {
result.append(try await field.validate(req: req))
}
return result.filter { $0 == false }.isEmpty
}

open func write(req: Request) async throws {


for field in fields {
try await field.write(req: req)
}
}

open func save(req: Request) async throws {


for field in fields {
try await field.save(req: req)
}
}

open func read(req: Request) async throws {


for field in fields {
try await field.read(req: req)
}
}

open func render(req: Request) -> TemplateRepresentable {


FormTemplate(
.init(
action: action,
fields: fields.map { $0.render(req: req)},
error: error,
submit: submit
)
)
}
}

Now that we're ready with our AbstractForm implementation, we should refactor our user
module just a bit. Let's create a new Forms directory inside the module so we can make a
UserLoginForm.

REFACTORING THE USER LOGIN


The very rst step is to alter the UserLoginContext object. We're going to use the form as a
TemplateRepresentable variable, so we can render it as a tag inside a template.

/// FILE: Sources/App/Modules/User/Templates/Contexts/UserLoginContext.swift

struct UserLoginContext {

let icon: String


let title: String
let message: String
let form: TemplateRepresentable

init(
icon: String,
title: String,
message: String,
form: TemplateRepresentable
) {

92
fi
self.icon = icon
self.title = title
self.message = message
self.form = form
}
}

The UserLoginTemplate needs some minor updates since we've changed the context.

/// FILE: Sources/App/Modules/User/Templates/Html/UserLoginTemplate.swift

import Vapor
import SwiftHtml

struct UserLoginTemplate: TemplateRepresentable {

var context: UserLoginContext

init(
_ context: UserLoginContext
) {
self.context = context
}

@TagBuilder
func render(
_ req: Request
) -> Tag {
WebIndexTemplate(
.init(title: context.title)
) {
Div {
Section {
P(context.icon)
H1(context.title)
P(context.message)
}
.class("lead")

context.form.render(req)
}
.id("user-login")
.class("container")
}
.render(req)
}
}

We also have to create a UserLoginForm with two input elds. We can create a convenience
init method where we specify the action method and URL and also the name of the submit
button. Inside the create elds method we can use the con g block to con gure the input
type as needed.

/// FILE: Sources/App/Modules/User/Forms/UserLoginForm.swift

import Vapor

final class UserLoginForm: AbstractForm {

public convenience init() {


self.init(
action: .init(
method: .post,
url: "/sign-in/"
),
submit: "Sign in"
)
self.fields = createFields()

93
fi
fi
fi
fi
}

func createFields() -> [FormComponent] {


[
InputField("email")
.config {
$0.output.context.label.required = true
$0.output.context.type = .email
},
InputField("password")
.config {
$0.output.context.label.required = true
$0.output.context.type = .password
}
]
}
}

We can remove the input object from the UserLoginController and use the newly created
form to render the view. The renderSignInView method is going to be changed, we're going
to pass the form as a parameter and call the render method on it to set it up as a context
variable.

The signInView function will simply initialize a new form, and the signInAction function
should also process the input since we'd like to set back the input values after a user
submission event. We're also going to use the generic error variable to tell the user that
something went wrong during the authentication.

/// FILE: Sources/App/Modules/User/Controllers/UserFrontendController.swift

import Vapor

struct UserFrontendController {

private func renderSignInView(


_ req: Request,
_ form: UserLoginForm
) -> Response {
let template = UserLoginTemplate(
.init(
icon: "⬇ ",
title: "Sign in",
message: "Please log in with your existing account",
form: form.render(req: req)
)
)
return req.templates.renderHtml(template)
}

func signInView(
_ req: Request
) async throws -> Response {
renderSignInView(req, .init())
}

func signInAction(
_ req: Request
) async throws -> Response {
/// the user is authenticated, we can store the user data inside the session too
if let user = req.auth.get(AuthenticatedUser.self) {
req.session.authenticate(user)
return req.redirect(to: "/")
}
let form = UserLoginForm()
try await form.process(req: req)
form.error = "Invalid email or password."
return renderSignInView(req, form)
}

94
func signOut(
_ req: Request
) throws -> Response {
req.auth.logout(AuthenticatedUser.self)
req.session.unauthenticate(AuthenticatedUser.self)
return req.redirect(to: "/")
}
}

Those are the changes that we had to make to take advantage of our new form framework.
As you can see we were able to remove quite a lot of code from the
UserFrontendController and we moved out the form-related logic into a separate le.

FORM COMPONENT BUILDER


There's one more thing that I'd like to show you in this chapter. Inside the UserLoginForm in
the createFields method we've returned an array of form elds, but I don't like it that we
always have to write the brackets and a colon in between the elements.

Fortunately Swift has a nice feature called result builders that we can use to make our code
a little bit more beautiful. We're going to create a FormComponentBuilder that can be used
to build an array of form elds.

/// FILE: Sources/App/Framework/Form/FormComponentBuilder.swift

@resultBuilder
public enum FormComponentBuilder {

public static func buildBlock(


_ components: FormComponent...
) -> [FormComponent] {
components
}
}

Now it's possible to mark the createFields method with the @FormComponentBuilder result
builder, and we can remove the bracket and column characters.

/// FILE: Sources/App/Modules/User/Forms/UserLoginForm.swift

import Vapor

final class UserLoginForm: AbstractForm {

public convenience init() {


self.init(
action: .init(
method: .post,
url: "/sign-in/"
),
submit: "Sign in"
)
self.fields = createFields()
}

@FormComponentBuilder
func createFields() -> [FormComponent] {
InputField("email")

95
fi
fi
fi
.config {
$0.output.context.label.required = true
$0.output.context.type = .email
}
InputField("password")
.config {
$0.output.context.label.required = true
$0.output.context.type = .password
}
}
}

Result builders are a really powerful feature in Swift. It's possible to create an entirely new
Domain Speci c Language (DSL) inside of Swift. The SwiftHtml template engine also takes
advantage of result builders, this makes it possible to de ne HTML views in a much more
elegant way.

SUMMARY
In this chapter we've learned how to build a reusable form component system. Now it's
possible to reuse labels and forms since we have corresponding context and template
objects. We've created a basic input eld and refactored the user module to take advantage
of the new mechanism.

96
fi
fi
fi
CHAPTER 7:
FORM EVENTS AND ASYNC VALIDATION

In the rst part of the chapter, we're going to work a little bit on our form components. We're
going to implement more event handler methods and you're going to learn the preferred
way of calling them to build a proper create or update work ow ow. The second half of the
chapter is all about building an asynchronous validation mechanism for the abstract forms.
We're going to build several form eld validators, and nally, you'll see how to work with
these validators and display user errors to improve the overall experience.

FORM EVENT HANDLERS


Let’s start by creating the new folders we’ll use during this chapter. You can use the
commands below to add them:

cd ~/myProject
mkdir -p Sources/App/Framework/Validation
mkdir -p Sources/App/Validation

In the previous chapter, we created a user login form. The main idea was that we were going
to create a template with a context and a view, much in the same way we create a model
with an object for each input eld, so we could compose various forms using just a few lines
of Swift code.

Now we have the foundation blocks for this and we're able to process user input, but we still
haven't implemented some other methods of the FormFieldComponent protocol. Let's start
doing this by using a common pattern that we're going to follow for almost every single
event method.

/// FILE: Sources/App/Framework/Form/AbstractFormField.swift

import Vapor

open class AbstractFormField<


Input: Decodable,
Output: TemplateRepresentable
>: FormComponent {

public var key: String


public var input: Input
public var output: Output
public var error: String?

// MARK: - event blocks

public typealias FormFieldBlock =


(Request, AbstractFormField<Input, Output>) async throws -> Void

private var readBlock: FormFieldBlock?


private var writeBlock: FormFieldBlock?
private var loadBlock: FormFieldBlock?

97
fi
fi
fi
fi
fl
fl
private var saveBlock: FormFieldBlock?

// MARK: - init & config

public init(
key: String,
input: Input,
output: Output,
error: String? = nil
) {
self.key = key
self.input = input
self.output = output
self.error = error
}

open func config(


_ block: (AbstractFormField<Input, Output>) -> Void
) -> Self {
block(self)
return self
}

// MARK: - Block setters

open func read(_ block: @escaping FormFieldBlock) -> Self {


readBlock = block
return self
}

open func write(_ block: @escaping FormFieldBlock) -> Self {


writeBlock = block
return self
}

open func load(_ block: @escaping FormFieldBlock) -> Self {


loadBlock = block
return self
}

open func save(_ block: @escaping FormFieldBlock) -> Self {


saveBlock = block
return self
}

// MARK: - FormComponent

open func process(req: Request) async throws {


if let value = try? req.content.get(Input.self, at: key) {
input = value
}
}

open func validate(req: Request) async throws -> Bool {


true
}

open func read(req: Request) async throws {


try await readBlock?(req, self)
}

open func write(req: Request) async throws {


try await writeBlock?(req, self)
}

open func load(req: Request) async throws {


try await loadBlock?(req, self)
}

open func save(req: Request) async throws {


try await saveBlock?(req, self)
}

open func render(req: Request) -> TemplateRepresentable {

98
output
}
}

As you can see, we've introduced a new typealias called FormFieldBlock. It's just a
convenience alias for an async throwing function that takes a Request and a generic
AbstractFormField<Input, Output> argument. By using this alias, we can simplify the other
function signatures a lot.

We've also created four new optional FormFieldBlock variables to handle various events.
These variables are private, so we need four new setter methods to be able to give new
values to them. The setter methods will work like builders or modi ers; after setting the
proper block value, we're going to return the current instance.

Inside the FormFieldComponent methods, we simply call the private event blocks if they're
not nil; otherwise, we don't have to perform any actions. This pattern will allow us to set up
form elds and de ne event handlers for them right away; let me show you an example.

InputField("name")
.load {
$1.output.context.value = "John Doe"
}
.save {
print("Hello, my name is \($1.input)!")
}

In this case, we can use the load method to update the output context value of the eld and
inside the "save" method we can also operate to process the input. The read / write
functions serve a similar purpose: the di erence is the order of execution.

FormFieldComponent events suggested order of execution:

When displaying the form:

- load

- read

- render

When handling a submission event:

- load

- process

- validate

- render if invalid

- write

- save

99
fi
fi
ff
fi
fi
This is going to be our work ow inside our admin controllers, but before we can implement a
CMS we still have to take care of form validation.

ASYNC FORM VALIDATION


One of the most important things is to validate incoming form elds. Vapor has a built-in
validation API to validate all sorts of input data, but the system has quite a lot of problems:

- You can't provide custom error messages

- The validation error detail is always a concatenated string (if there are multiple errors)

- You can't get back the error message for a given key from the error detail string

- Validation happens synchronously (you can't validate based on a db query)

This is very unfortunate because Vapor has some real nice validator functions, but they're
focusing more on API validation rather than form validation. Not only that, the underlying
error detail message that's returned (as a RESTful JSON result) is quite unusable.

We're going to build a set of uni ed async validation helpers that can be used both for form
and API validation purposes. We're going to talk more about API validation as well, but for
now, let's just focus on HTML forms and validating input elds.

The very rst thing that we're going to need is a keyed error detail object with an associated
error message. Create a new ValidationErrorDetail Swift le and place it into the new
Framework/Validation folder.

/// FILE: Sources/App/Framework/Validation/ValidationErrorDetail.swift

import Vapor

public struct ValidationErrorDetail: Codable {

public var key: String


public var message: String

public init(
key: String,
message: String
) {
self.key = key
self.message = message
}
}

extension ValidationErrorDetail: Content {}

We're going to use this object to uniquely identify the invalid form eld based on the key.

Now we need a protocol that we can use to validate form elds generically. We're going to
require a key and a message plus an async validate function that can return an optional
ValidationErrorDetail object if there was an error.

100
fi
fl
fi
fi
fi
fi
fi
fi
Please note that this function can throw, but we're only going to throw an error if a system
error happened, such as a database failure or something similar. We always return user-
related errors as a ValidationErrorDetail object or a nil value if everything was ne.

/// FILE: Sources/App/Framework/Validation/AsyncValidator.swift

import Vapor

public protocol AsyncValidator {

var key: String { get }


var message: String { get }

func validate(
_ req: Request
) async throws -> ValidationErrorDetail?
}

public extension AsyncValidator {

var error: ValidationErrorDetail {


.init(key: key, message: message)
}
}

We can also de ne a little helper variable that can construct an error object based on the
given key and message. This protocol will be great for validating a single form eld, but
usually, we'd like to validate an entire request. We can do that by creating a request validator
that can use underlying async validators, but before we do that, let's create one more extra
thing.

As I mentioned earlier, it'd be nice to use the same validation objects for API and form eld
validation. The reason why we're going to create a new ValidationAbort struct is that the
default validation response won't contain the necessary info about the errors, but our
ValidationErrorDetail has more details about the problematic key and also features a proper
error message.

/// FILE: Sources/App/Framework/Validation/ValidationAbort.swift

import Vapor

public struct ValidationAbort: AbortError {

public var abort: Abort


public var message: String?
public var details: [ValidationErrorDetail]

public var reason: String { abort.reason }


public var status: HTTPStatus { abort.status }

public init(
abort: Abort,
message: String? = nil,
details: [ValidationErrorDetail]
) {
self.abort = abort
self.message = message
self.details = details
}
}

The ValidationAbort type is going to implement Vapor's AbortError protocol, which is an


Error so we can throw it and the system can convert it to a proper HTTP response if needed.
101
fi
fi
fi
fi
We add an abort property so we can return with a custom status code and a generic error
message, just like we did for our AbstractForm object. We also include the details; that array
is going to contain all the issues that we had with the request.

Inside the RequestValidator, we're going to call the validate methods on the array of the
AsyncValidator protocol objects. We can optimize the process by checking the keys in the
results array, so if a eld associated with a given key is already invalid, we don't have to run
the remaining validators for that. Also, if the request validator fails, it means there are errors
in the result array, and we can throw a ValidationAbort.

/// FILE: Sources/App/Framework/Validation/RequestValidator.swift

import Vapor

public struct RequestValidator {

public var validators: [AsyncValidator]

public init(
_ validators: [AsyncValidator]
) {
self.validators = validators
}

public func validate(


_ req: Request,
message: String? = nil
) async throws {
var result: [ValidationErrorDetail] = []
for validator in validators {
if result.contains(where: { $0.key == validator.key }) {
continue
}
if let res = try await validator.validate(req) {
result.append(res)
}
}
if !result.isEmpty {
throw ValidationAbort(
abort: Abort(.badRequest, reason: message),
details: result
)
}
}

public func isValid(


_ req: Request
) async -> Bool {
do {
try await validate(req, message: nil)
return true
}
catch {
return false
}
}
}

Instead of returning the array of ValidationErrorDetail objects, this time we always throw an
error because we're going to need an abort error for JSON-related APIs. We still can use this
method and check if a request is valid or not by trying the validate method. If that call fails
we can return with a false value; otherwise, the incoming request was ne so we return true.

102
fi
fi
Now that we can validate things asynchronously, and we can validate an entire request
object, it's time to come up with a validator that can check an input value and pass the error
message to a given form eld as an output. We'll call this a FormFieldValidator object: it's a
generic struct that has an associated Decodable input and a TemplateRepresentable output
(just like an AbstractFormField) type and it conforms to the AsyncValidator protocol (of
course).

/// FILE: Sources/App/Validation/FormFieldValidator.swift

import Vapor

public struct FormFieldValidator<


Input: Decodable,
Output: TemplateRepresentable
>: AsyncValidator {

public typealias AsyncValidationBlock =


((Request, AbstractFormField<Input, Output>) async throws -> Bool)

public let field: AbstractFormField<Input, Output>


public let message: String
public let validation: AsyncValidationBlock

public var key: String { field.key }

public init(
_ field: AbstractFormField<Input, Output>,
_ message: String,
_ validation: @escaping AsyncValidationBlock
) {
self.field = field
self.message = message
self.validation = validation
}

public func validate(


_ req: Request
) async throws -> ValidationErrorDetail? {
let isValid = try await validation(req, field)
if isValid {
return nil
}
field.error = message
return error
}
}

The init method will accept three arguments, the rst one is a pointer to the
AbstractFormField instance, the second one is an error message, and the third one is a
validation block that we're going to run when we have to validate the input. Inside the
validate method we simply call the stored validation block. If the input was valid: end of
story... we return with a nil value. If there was an error, we set the error message on the eld
by using the reference and return the error details as a result.

The great thing about this approach is that we can still use the built-in Vapor validator
methods and create helper methods to validate our form elds based on the input type. For
example, String validation is quite a common use case, so it makes sense to de ne an
extension.

/// FILE: Sources/App/Validation/FormFieldValidator+Validations.swift

import Vapor

public extension FormFieldValidator where Input == String {

103
fi
fi
fi
fi
fi
static func required(
_ field: AbstractFormField<Input, Output>,
_ message: String? = nil
) -> FormFieldValidator<Input, Output> {
let msg = message ??
"\(field.key.capitalized) is required"
return .init(field, msg) { _, field in
!field.input.isEmpty
}
}

static func min(


_ field: AbstractFormField<Input, Output>,
length: Int,
message: String? = nil
) -> FormFieldValidator<Input, Output> {
let msg = message ??
"\(field.key.capitalized) is too short (min: \(length) characters)"
return .init(field, msg) { _, field in
field.input.count >= length
}
}

static func max(


_ field: AbstractFormField<Input, Output>,
length: Int,
message: String? = nil
) -> FormFieldValidator<Input, Output> {
let msg = message ??
"\(field.key.capitalized) is too short (min: \(length) characters)"
return .init(field, msg) { _, field in
field.input.count <= length
}
}

static func alphanumeric(


_ field: AbstractFormField<Input, Output>,
message: String? = nil
) -> FormFieldValidator<Input, Output> {
let msg = message ??
"\(field.key.capitalized) should be only alphanumeric characters"
return .init(field, msg) { _, field in
!Validator
.characterSet(.alphanumerics)
.validate(field.input)
.isFailure
}
}

static func email(


_ field: AbstractFormField<Input, Output>,
message: String? = nil
) -> FormFieldValidator<Input, Output> {
let msg = message ??
"\(field.key.capitalized) should be a valid email address"
return .init(field, msg) { _, field in
!Validator
.email
.validate(field.input)
.isFailure
}
}
}

Before we change the AbstractFormField component, we're going to add one more
convenient enum that we can use to return an array of AsyncValidator objects via result
builders.

/// FILE: Sources/App/Validation/AsyncValidatorBuilder.swift

104
@resultBuilder
public enum AsyncValidatorBuilder {

public static func buildBlock(


_ components: AsyncValidator...
) -> [AsyncValidator] {
components
}
}

Inside the abstract form eld le, we'll add a new FormFieldValidatorBlock type that we can
use to return an array of AsyncValidator items. In addition to this, we're going to add a new
validators setter function that can be used to de ne the async validators when we set up a
form eld. Finally, we can implement the validate method for real this time and create a
RequestValidator with the async validators, then we can call the isValid method and return
the result as a boolean value.

/// FILE: Sources/App/Framework/Form/AbstractFormField.swift

import Vapor

open class AbstractFormField<


Input: Decodable,
Output: TemplateRepresentable
>: FormComponent {

public var key: String


public var input: Input
public var output: Output
public var error: String?

// MARK: - event blocks

public typealias FormFieldBlock =


(Request, AbstractFormField<Input, Output>) async throws -> Void
public typealias FormFieldValidatorsBlock =
((Request, AbstractFormField<Input, Output>) -> [AsyncValidator])

private var readBlock: FormFieldBlock?


private var writeBlock: FormFieldBlock?
private var loadBlock: FormFieldBlock?
private var saveBlock: FormFieldBlock?
private var validatorsBlock: FormFieldValidatorsBlock?

// MARK: - init & config

public init(
key: String,
input: Input,
output: Output,
error: String? = nil
) {
self.key = key
self.input = input
self.output = output
self.error = error
}

open func config(


_ block: (AbstractFormField<Input, Output>) -> Void
) -> Self {
block(self)
return self
}

// MARK: - Block setters

open func read(_ block: @escaping FormFieldBlock) -> Self {


readBlock = block

105
fi
fi
fi
fi
return self
}

open func write(_ block: @escaping FormFieldBlock) -> Self {


writeBlock = block
return self
}

open func load(_ block: @escaping FormFieldBlock) -> Self {


loadBlock = block
return self
}

open func save(_ block: @escaping FormFieldBlock) -> Self {


saveBlock = block
return self
}

open func validators(


@AsyncValidatorBuilder _ block: @escaping FormFieldValidatorsBlock
) -> Self {
validatorsBlock = block
return self
}

// MARK: - FormComponent

open func process(req: Request) async throws {


if let value = try? req.content.get(Input.self, at: key) {
input = value
}
}

open func validate(req: Request) async throws -> Bool {


guard let validators = validatorsBlock else {
return true
}
return await RequestValidator(validators(req, self)).isValid(req)
}

open func read(req: Request) async throws {


try await readBlock?(req, self)
}

open func write(req: Request) async throws {


try await writeBlock?(req, self)
}

open func load(req: Request) async throws {


try await loadBlock?(req, self)
}

open func save(req: Request) async throws {


try await saveBlock?(req, self)
}

open func render(req: Request) -> TemplateRepresentable {


output
}
}

We've created quite a lot of validation-related methods and objects, but now we can easily
put validators on form elds. Let's update our UserLoginForm and validate the email
address and password elds for real. Both elds are required, but we're also going to check
if the email eld is a proper email address or not.

/// FILE: Sources/App/Modules/User/Forms/UserLoginForm.swift

import Vapor

106
fi
fi
fi
fi
final class UserLoginForm: AbstractForm {

public convenience init() {


self.init(
action: .init(
method: .post,
url: "/sign-in/"
),
submit: "Sign in"
)
self.fields = createFields()
}

@FormComponentBuilder
func createFields() -> [FormComponent] {
InputField("email")
.config {
$0.output.context.label.required = true
$0.output.context.type = .email
}
.validators {
FormFieldValidator.required($1)
FormFieldValidator.email($1)
}
InputField("password")
.config {
$0.output.context.label.required = true
$0.output.context.type = .password
}
.validators {
FormFieldValidator.required($1)
}
}
}

Inside the UserFrontendController we still have to call the validate method on the form. If
there was an error we don't have to set a generic form error message, because there's going
to be a eld error displayed next to the input elds, but if there were no errors and we still
reached this point, that would mean the email or password was incorrect, so we would set a
generic message.

/// FILE: Sources/App/Modules/User/Controllers/UserFrontendController.swift

import Vapor

struct UserFrontendController {

private func renderSignInView(


_ req: Request,
_ form: UserLoginForm
) -> Response {
let template = UserLoginTemplate(
.init(
icon: "⬇ ",
title: "Sign in",
message: "Please log in with your existing account",
form: form.render(req: req)
)
)
return req.templates.renderHtml(template)
}

func signInView(
_ req: Request
) async throws -> Response {
renderSignInView(req, .init())
}

func signInAction(

107
fi
fi
_ req: Request
) async throws -> Response {
/// the user is authenticated, we can store the user data inside the session too
if let user = req.auth.get(AuthenticatedUser.self) {
req.session.authenticate(user)
return req.redirect(to: "/")
}
let form = UserLoginForm()
try await form.process(req: req)
let isValid = try await form.validate(req: req)
if !isValid {
form.error = "Invalid email or password."
}
return renderSignInView(req, form)
}

func signOut(
_ req: Request
) throws -> Response {
req.auth.logout(AuthenticatedUser.self)
req.session.unauthenticate(AuthenticatedUser.self)
return req.redirect(to: "/")
}
}

Now if you run the project, you should see that we have a lot better user experience. If the
login form had a missing input value the user will know it. If both elds were lled but the
credentials were incorrect then we're going to display just a single error message.

Setting up validators with this method is ridiculously simple, you can add more validator
functions as extensions on the AbstractFormField class but you can also come up with
custom validators.

SUMMARY
This chapter was all about nishing up form management. Now that we've got proper event
handlers and we can also validate form elds, it's time to introduce even more form elds,
because we're going to need a lot more than just a single input eld if we want to build a
fully functioning CMS. In the next chapter, we're going to introduce some simple and more
advanced form elds.

108
fi
fi
fi
fi
fi
fi
fi
CHAPTER 8:
ADVANCED FORM FIELDS

This chapter is going to be all about advanced form elds. We're going to create a set of
new eld types that we're going to use later on. You'll learn how to build custom form elds
based on the abstract form eld class, so by the end of this chapter, you should be able to
create even more form elds to t your needs. We're also going to introduce a brand new
Swift package called Liquid, which is a le storage driver made for Vapor. By using this
library, we'll be able to create a form eld for uploading images.

HIDDEN FIELD
Let’s start by creating the new folders we’ll use during this chapter. You can use the
commands below to add them:

cd ~/myProject
mkdir -p Sources/App/Framework/Extensions

A hidden eld is something that's not visible to the end-user, but we can still use it to submit
data through our form. It's a pretty simple eld type we only need a key and an optional
value in our HiddenFieldContext object.

/// FILE: Sources/App/Framework/Form/Fields/HiddenFieldContext.swift

public struct HiddenFieldContext {

public let key: String


public var value: String?

public init(key: String, value: String? = nil) {


self.key = key
self.value = value
}
}

The corresponding HiddenFieldTemplate is also very minimal, we only have to con gure an
Input eld with a .hidden type and the values using the context.

/// FILE: Sources/App/Framework/Form/Fields/HiddenFieldTemplate.swift

import Vapor
import SwiftHtml

public struct HiddenFieldTemplate: TemplateRepresentable {

var context: HiddenFieldContext

public init(
_ context: HiddenFieldContext
) {
self.context = context

109
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
}

@TagBuilder
public func render(
_ req: Request
) -> Tag {
Input()
.type(.hidden)
.name(context.key)
.value(context.value)
}
}

The third component is the actual HiddenField class, the input will be a String and we can
use the HiddenFieldTemplate as an output type. Inside the process method, we return the
output context value to the already processed input value.

/// FILE: Sources/App/Framework/Form/Fields/HiddenField.swift

import Vapor

public final class HiddenField: AbstractFormField<


String,
HiddenFieldTemplate
> {

public convenience init(_ key: String) {


self.init(
key: key,
input: "",
output: .init(
.init(key: key)
)
)
}

public override func process(


req: Request
) async throws {
try await super.process(req: req)
output.context.value = input
}
}

That's it, we're ready with our brand new input eld. This was a quite simple addition to our
system, but we're going to use it quite a lot in the long term.

TEXTAREA FIELD
The TextareaField is always going to represent a textual input eld, no matter what. We'll
follow the same pattern for this eld type too. First, we should create a new struct for the
TextareaFieldContext object.

/// FILE: Sources/App/Framework/Form/Fields/TextareaFieldContext.swift

public struct TextareaFieldContext {

public let key: String


public var label: LabelContext
public var placeholder: String?
public var value: String?
public var error: String?

110
fi
fi
fi
public init(
key: String,
label: LabelContext? = nil,
placeholder: String? = nil,
value: String? = nil,
error: String? = nil
) {
self.key = key
self.label = label ?? .init(key: key)
self.placeholder = placeholder
self.value = value
self.error = error
}
}

The Textarea context is quite similar to the input context, but this time we can omit the type
parameter since a Textarea has no type. Apart from this di erence, everything else is the
same.

Now we should also create a template le for the Textarea eld.

/// FILE: Sources/App/Framework/Form/Fields/TextareaFieldTemplate.swift

import Vapor
import SwiftHtml

public struct TextareaFieldTemplate: TemplateRepresentable {

public var context: TextareaFieldContext

public init(
_ context: TextareaFieldContext
) {
self.context = context
}

@TagBuilder
public func render(
_ req: Request
) -> Tag {
LabelTemplate(context.label).render(req)

Textarea(context.value)
.placeholder(context.placeholder)
.name(context.key)

if let error = context.error {


Span(error)
.class("error")
}
}
}

Just like in the InputFieldTemplate we can reuse the common LabelTemplate to render the
details of the label, and we can use a Textarea tag to con gure our view. Finally, if there's
any error we display it using a Span tag with an error class.

Lastly, we have to create a TextareaField as a subclass of an AbstractFormField.

/// FILE: Sources/App/Framework/Form/Fields/TextareaField.swift

import Vapor

public final class TextareaField: AbstractFormField<

111
fi
fi
ff
fi
String,
TextareaFieldTemplate
> {

public convenience init(_ key: String) {


self.init(
key: key,
input: "",
output: .init(
.init(key: key)
)
)
}

public override func process(


req: Request
) async throws {
try await super.process(req: req)
output.context.value = input
}

public override func render(


req: Request
) -> TemplateRepresentable {
output.context.error = error
return super.render(req: req)
}
}

After processing the input value, we can update the output context with it, and before we
render the template we should assign the current error value to the output context too.

SELECT FIELD
The select eld is going to be a bit more complicated. This eld utilizes a select HTML
element with multiple available options. Every option should have a key and a label and
because this is a commonly reused component we're going to create a standalone
OptionContext to represent it.

/// FILE: Sources/App/Framework/Form/Templates/Contexts/OptionContext.swift

public struct OptionContext {

public var key: String


public var label: String

public init(
key: String,
label: String
) {
self.key = key
self.label = label
}
}

The nice thing about this option context struct is that you can de ne additional helper
methods to cover common cases or option values, such as a yes / no selection or a set of
numbers.

/// FILE: Sources/App/Framework/Form/Templates/Contexts/OptionContext.swift

112
fi
fi
fi
// ...

public extension OptionContext {

static func yesNo() -> [OptionContext] {


["yes", "no"].map {
.init(key: $0, label: $0.capitalized)
}
}

static func trueFalse() -> [OptionContext] {


[true, false].map {
.init(key: String($0), label: String($0).capitalized)
}
}

static func numbers(


_ numbers: [Int]
) -> [OptionContext] {
numbers.map {
.init(key: String($0), label: String($0))
}
}
}

The SelectFieldContext is going to feature an array of options and a possible value that can
be used to mark an option as selected if the option key and the value matches. Apart from
these two properties, the context will have the other regular values, such as the label context
and the error.

/// FILE: Sources/App/Framework/Form/Fields/SelectFieldContext.swift

public struct SelectFieldContext {

public let key: String


public var label: LabelContext
public var options: [OptionContext]
public var value: String?
public var error: String?

public init(
key: String,
label: LabelContext? = nil,
options: [OptionContext] = [],
value: String? = nil,
error: String? = nil
) {
self.key = key
self.label = label ?? .init(key: key)
self.options = options
self.value = value
self.error = error
}
}

Inside the SelectFieldTemplate we have to iterate through the options and map them into
Option tags. We can simply set the value to the key and name the options using the label.
The selected modi er is ideal to set the selected value if the context value matches the
item's key.

/// FILE: Sources/App/Framework/Form/Fields/SelectFieldTemplate.swift

import Vapor
import SwiftHtml

113
fi
public struct SelectFieldTemplate: TemplateRepresentable {

public var context: SelectFieldContext

public init(
_ context: SelectFieldContext
) {
self.context = context
}

@TagBuilder
public func render(
_ req: Request
) -> Tag {
LabelTemplate(context.label).render(req)

Select {
for item in context.options {
Option(item.label)
.value(item.key)
.selected(context.value == item.key)
}
}
.name(context.key)

if let error = context.error {


Span(error)
.class("error")
}
}
}

The last step is to create the regular form eld class, this should look very familiar by now.

/// FILE: Sources/App/Framework/Form/Fields/SelectField.swift

import Vapor

public final class SelectField: AbstractFormField<


String,
SelectFieldTemplate
> {

public convenience init(_ key: String) {


self.init(
key: key,
input: "",
output: .init(
.init(key: key)
)
)
}

public override func process(


req: Request
) async throws {
try await super.process(req: req)
output.context.value = input
}

public override func render(


req: Request
) -> TemplateRepresentable {
output.context.error = error
return super.render(req: req)
}
}

114
fi
As you can see creating new form elds is a pretty straightforward process: every time you
need a context, a template, and a form eld object to connect the context with the template.
You should create a ToggleField or a RadioField using the checkbox HTML element if you
want to practice. A toggle eld can also use a Bool value as an input type, since it's a true/
false selector, on the other hand, the radio eld works like a select eld. You can also think
about how to handle multiple element selection. Hint: usually I create a separate eld entity
for that purpose.

FILE UPLOADS & IMAGE FIELD


The name of this chapter suggests that we're going to deal with some more advanced form
elds and this is the point when things start to get a bit more complicated. We're going to
build an image upload eld, but to upload a le to the server we need a little help. It's
possible to move les to the server from a client by using Vapor, but fortunately, there's a
better way to handle le uploads.

There's a le storage component called Liquid which makes asset management a lot easier.
You can think about it like Fluent, it's an abstraction with multiple storage driver support. You
can upload les directly to your server by using the local driver, but it's also possible to store
les inside an AWS S3 bucket by using the S3 driver.

Liquid les are saved inside the storage using a unique key. The key is usually a relative le
path including the folder structure, e.g. foo/bar/baz.jpg. This way the system can resolve the
full location of the le no matter the storage driver. You'll see how this works in practice later
on.

To use Liquid we have to add the package to the project as a dependency.

/// FILE: Package.swift

// swift-tools-version:5.7
import PackageDescription

let package = Package(


name: "myProject",
platforms: [
.macOS(.v12)
],
dependencies: [
.package(
url: "https://github.com/vapor/vapor",
from: "4.70.0"
),
.package(
url: "https://github.com/vapor/fluent",
from: "4.4.0"
),
.package(
url: "https://github.com/vapor/fluent-sqlite-driver",
from: "4.1.0"
),
.package(
url: "https://github.com/binarybirds/liquid",
from: "1.3.0"
),
.package(

115
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
url: "https://github.com/binarybirds/liquid-local-driver",
from: "1.3.0"
),
.package(
url: "https://github.com/binarybirds/swift-html",
from: "1.7.0"
),
],
targets: [
.target(name: "App", dependencies: [
.product(name: "Vapor", package: "vapor"),
.product(name: "Fluent", package: "fluent"),
.product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"),
.product(name: "Liquid", package: "liquid"),
.product(name: "LiquidLocalDriver", package: "liquid-local-driver"),
.product(name: "SwiftHtml", package: "swift-html"),
.product(name: "SwiftSvg", package: "swift-html"),
]),
.executableTarget(name: "Run", dependencies: ["App"]),
.testTarget(name: "AppTests", dependencies: [
.target(name: "App"),
.product(name: "XCTVapor", package: "vapor"),
])
]
)

The con guration is very simple and for the sake of simplicity, we're going to use the local
driver. The publicUrl parameter is the base URL of your publicly available les. It's going to
be used to resolve le keys. The publicPath is the location of the public folder and the
workDirectory is going to be used under the public folder as a root directory to store les.

/// FILE: Sources/App/con gure.swift

import Vapor
import Fluent
import FluentSQLiteDriver
import Liquid
import LiquidLocalDriver

public func configure(


_ app: Application
) throws {

app.fileStorages.use(
.local(
publicUrl: "http://localhost:8080",
publicPath: app.directory.publicDirectory,
workDirectory: "assets"
),
as: .local
)

app.routes.defaultMaxBodySize = "10mb"

// ...

To be able to collect the uploaded data, we also have to set the defaultMaxBodySize value
on the app.routes property. A value of "10MB" will be more than enough for now. Please
note that this con guration will change the max body size globally, but it's possible to
change it only for those routes that you're going to use to upload les. Usually, that's a better
practice, but this time we'll be just ne by altering the global settings.

116
fi
fi
fi
fi
fi
fi
fi
fi
Before we move into the InputField, we still have to do some preparation work.
Unfortunately, sometimes Vapor has strange naming conventions: the data value of the le
type expressly represents a ByteBu er object, so let's create an alias for that property real
quick.

/// FILE: Sources/App/Framework/Extensions/File+ByteBu er.swift

import Vapor

public extension File {

var byteBuffer: ByteBuffer { data }


}

It is also very convenient to create an optional data extension for the ByteBu er type, this
way we can return the entire data contained by the bu er.

/// FILE: Sources/App/Framework/Extensions/ByteBu er+Data.swift

import Vapor

public extension ByteBuffer {

var data: Data? {


getData(at: 0, length: readableBytes)
}
}

So, what kind of data do we need when we try to upload an image?

It would be nice to see the original image if there was any when we render the form, so we
need something to represent the original image key. We should be able to upload the le, so
we need temporary le storage where we can store the new key and name values.
Sometimes we just want to get rid of the image and for this purpose, we can introduce a
simple Bool ag.

Let's create a new FormImageData type that represents this structure, we should conform to
the Codable* protocol since we might want to try to encode or decode it.

/// FILE: Sources/App/Framework/Form/FormImageData.swift

public struct FormImageData: Codable {

public struct TemporaryFile: Codable {


public let key: String
public let name: String

public init(
key: String,
name: String
) {
self.key = key
self.name = name
}
}

public var originalKey: String?


public var temporaryFile: TemporaryFile?
public var shouldRemove: Bool

public init(
originalKey: String? = nil,

117
fl
fi
ff
ff
ff
ff
ff
fi
fi
temporaryFile: TemporaryFile? = nil,
shouldRemove: Bool = false
) {
self.originalKey = originalKey
self.temporaryFile = temporaryFile
self.shouldRemove = shouldRemove
}
}

Apart from the regular key, label, and error values we're going to use this FormImageData
as a data object inside the ImageFieldContext struct. We also going to feature a previewUrl
and an accept property to set up the template.

/// FILE: Sources/App/Framework/Form/Fields/Templates/Context/ImageFieldContext.swift

public struct ImageFieldContext {

public let key: String


public var label: LabelContext
public var data: FormImageData
public var previewUrl: String?
public var accept: String?
public var error: String?

public init(
key: String,
label: LabelContext? = nil,
data: FormImageData = .init(),
previewUrl: String? = nil,
accept: String? = nil,
error: String? = nil
) {
self.key = key
self.label = label ?? .init(key: key)
self.data = data
self.previewUrl = previewUrl
self.accept = accept
self.error = error
}
}

The ImageFieldTemplate is a bit longer than usual. In the very rst part of the render
template, we're going to try to display the previewUrl as an image if there's a URL value.

Next, we display the label as usual and we add a regular le input eld using the key and the
accept value from the context. With the accept value, you can restrict the le types that the
user can select during the upload, the value should be a valid media type, such as image/
png.

The temporary le is required when there was an error with the form during the submission.
If something goes wrong during the validation process we might lose the uploaded picture if
we won't re-submit the le key and the name again as an input value. This way even if some
other eld was incorrect we won't lose the uploaded image le, we just have to move the
temporary le to its nal location. This is the same reason why we might submit the original
key if there was any.

The very last input eld indicates if the user wants to remove the uploaded image.

/// FILE: Sources/App/Framework/Form/Fields/Templates/Html/ImageFieldTemplate.swift

118
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
import Vapor
import SwiftHtml

public struct ImageFieldTemplate: TemplateRepresentable {

public var context: ImageFieldContext

public init(
_ context: ImageFieldContext
) {
self.context = context
}

@TagBuilder
public func render(
_ req: Request
) -> Tag {

if let url = context.previewUrl {


Img(src: req.fs.resolve(key: url), alt: context.key)
} else {
Img(src: "/img/logo.png", alt: "default post image")
}

LabelTemplate(context.label).render(req)

Input()
.type(.file)
.key(context.key)
.class("field")
.accept(context.accept)

if let temporaryFile = context.data.temporaryFile {


Input()
.key(context.key + "TemporaryFileKey")
.value(temporaryFile.key)
.type(.hidden)

Input()
.key(context.key + "TemporaryFileName")
.value(temporaryFile.name)
.type(.hidden)
}

if let key = context.data.originalKey {


Input()
.key(context.key + "OriginalKey")
.value(key)
.type(.hidden)
}

if !context.label.required {
Input()
.key(context.key + "ShouldRemove")
.value(String(true))
.type(.checkbox)
.checked(context.data.shouldRemove)

Label("Remove")
.for(context.key + "Remove")
}

if let error = context.error {


Span(error)
.class("error")
}
}
}

119
Now that we can render an image eld, we still need the form eld subclass to be able to
process it and upload the le to the server. Before we move on to that part, we're going to
de ne one more helper object, which is going to be the input type for the abstract form eld.

The FormImageInput struct will have a key, a le value, which is going to represent the
uploaded le data, and a data object that's a FormImageData type.

/// FILE: Sources/App/Framework/Form/ImageInput.swift

import Vapor

public struct FormImageInput: Codable {

public var key: String


public var file: File?
public var data: FormImageData

public init(
key: String,
file: File? = nil,
data: FormImageData? = nil
) {
self.key = key
self.file = file
self.data = data ?? .init()
}
}

Now we can use the FormImageInput as an input value and the ImageFieldTemplate as an
output type when we create our ImageField. We're going to use a public imageKey variable
to store the current key and make it accessible for others as well. The path variable is going
to be the pre x of the image keys, it's just a directory path where we save the uploaded le.

The process function is going to be more interesting than it used to be for other eld types.
First, we try to decode the input based on the keys that we used in the template le. After
we have the complete input data, we check if the le should be removed or not and we
perform the corresponding action based on the other input values.

If the le should be removed and there was an original key that means we have to delete the
original le using the req.fs.delete(key:) method.

If there was some sort of image data that the user submitted, we should rst check the
temporary le and delete it based on the key, because we're going to upload the new data
to the server and store it as a temporary le rst.

You can upload les using Liquid by calling the try await req.fs.upload(key: key, data: data)
method. By default, it'll return the full URL of the uploaded le, but we don't care about that
now.

As the last step, we can update the output context data with the current input data and we're
done.

/// FILE: Sources/App/Framework/Form/Fields/ImageField.swift

import Vapor

120
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
public final class ImageField: AbstractFormField<
FormImageInput,
ImageFieldTemplate
> {

public var imageKey: String? {


didSet {
output.context.data.originalKey = imageKey
}
}

public var path: String

public init(
_ key: String,
path: String
) {
self.path = path
super.init(
key: key,
input: .init(key: key),
output: .init(.init(key: key))
)
}

public override func process(


req: Request
) async throws {
/// process input
input.file = try? req.content.get(
File.self,
at: key
)

input.data.originalKey = try? req.content.get(


String.self,
at: key + "OriginalKey"
)
if
let temporaryFileKey = try? req.content.get(
String.self,
at: key + "TemporaryFileKey"
),
let temporaryFileName = try? req.content.get(
String.self,
at: key + "TemporaryFileName"
)
{
input.data.temporaryFile = .init(
key: temporaryFileKey,
name: temporaryFileName
)
}
input.data.shouldRemove = (try? req.content.get(
Bool.self,
at: key + "ShouldRemove"
)) ?? false

/// remove & upload le


if input.data.shouldRemove {
if let originalKey = input.data.originalKey {
try? await req.fs.delete(key: originalKey)
}
}
else if
let file = input.file,
let data = file.byteBuffer.data,
!data.isEmpty
{
if let tmpKey = input.data.temporaryFile?.key {
try? await req.fs.delete(key: tmpKey)
}
let key = "tmp/\(UUID().uuidString).tmp"

121
fi
_ = try await req.fs.upload(key: key, data: data)
/// update the temporary image
input.data.temporaryFile = .init(
key: key,
name: file.filename
)
}
/// update output values
output.context.data = input.data

public override func write(


req: Request
) async throws {
imageKey = input.data.originalKey
if input.data.shouldRemove {
if let key = input.data.originalKey {
try? await req.fs.delete(key: key)
}
imageKey = nil
}
else if let file = input.data.temporaryFile {
var newKey = path + "/" + file.name
if await req.fs.exists(key: newKey) {
let formatter = DateFormatter()
formatter.dateFormat="y-MM-dd-HH-mm-ss-"
let prefix = formatter.string(from: .init())
newKey = path + "/" + prefix + file.name
}

_ = try await req.fs.move(key: file.key, to: newKey)


input.data.temporaryFile = nil
if let key = input.data.originalKey {
try? await req.fs.delete(key: key)
}
imageKey = newKey
}
try await super.write(req: req)
}

public override func render(


req: Request
) -> TemplateRepresentable {
output.context.error = error
return super.render(req: req)
}
}

The write function call happens after the validation step succeeded so it's now safe to move
the uploaded le to the nal destination. First, we have to check if there was a remove action
and if we have to perform this action we simply remove the le based on the original key.

Otherwise, we can be sure that the currently uploaded le is already stored as a temporary
le on the server and we can move it to the assets directory. The only trick is that if there's
an already existing le with the given key, we're going to pre x the le name with the current
timestamp.

Then we can move the temp le to the assets directory by using req.fs.move and delete the
original key if there was an existing one because we've just replaced that with the new one.

We store the nal key inside the imageKey property and we call super.write(req:) to handle
further actions. A lot is happening in this method and the same applies to the process
function too, but it's not that hard to understand once you examine the code step-by-step.
122
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
Fortunately, this complex internal mechanism will provide us with a simple API for the
ImageField.

ImageField("image", path: "blog/post")


.read {
if let key = model.imageKey {
$1.output.context.previewUrl = $0.fs.resolve(key: key)
}
($1 as! ImageField).imageKey = model.imageKey
}
.write {
model.imageKey = ($1 as! ImageField).imageKey
}

With just a few lines of code, we're going to be able to properly upload images.

SUMMARY
This chapter was all about introducing new form elds. We've created a hidden eld for
submitting invisible key-value pairs and we've added a Textarea eld for multi-line user
inputs. The select eld is a bit more complicated type with the ability to select a given value
from an array of options. In the second half of the chapter, we've added the Liquid le
storage driver to the project, which allows us to upload les to the server hassle-free. By
taking advantage of Liquid, we were able to de ne a brand new ImageField that will help us
upload image les and replace or remove them if we don't need them anymore. In the next
chapter, we're going to take advantage of these elds and we're going to create a basic
CMS interface for our blog module.

123
fi
fi
fi
fi
fi
fi
fi
fi
fi
CHAPTER 9:
CONTENT MANAGEMENT SYSTEM

Through this chapter, we're going to build a content management system with an admin
interface. We're going to create a standalone module for the admin views, which will be
completely separated from the web frontend. The CMS will support list, detail, create, update
and delete functionality. Models are going to be persisted to the database and we'll secure
the admin endpoints by using a new built-in middleware.

THE ADMIN MODULE


Let’s start by creating the new folders we’ll use during this chapter, and while we're at it,
we're also going to create a CSS and a JS le we'll use later. You can use the commands
below to add them:

cd ~/myProject
mkdir -p Sources/App/Extensions
mkdir -p Sources/App/Modules/Admin
mkdir -p Sources/App/Modules/Admin/Controllers
mkdir -p Sources/App/Modules/Admin/Templates
mkdir -p Sources/App/Modules/Admin/Templates/Contexts
mkdir -p Sources/App/Modules/Admin/Templates/Html
mkdir -p Sources/App/Modules/Blog/Forms
touch Public/css/admin.css
touch Public/js/admin.js

The admin URLs will follow this pattern:

- GET /admin/module/model/ - list

- GET /admin/module/model/:modelId/ - detail

- GET /admin/module/model/create/ - create view

- POST /admin/module/model/create/ - create action

- GET /admin/module/model/:modelId/update/ - update view

- POST /admin/module/model/:modelId/update/ - update action

- GET /admin/module/model/:modelId/delete/ - delete view

- POST /admin/module/model/:modelId/delete/ - delete action

Before we create the admin module, let's refactor our code just a little bit. First of all, we're
going to move out the Svg menu icon extension from the web index template le.

124
fi
fi
/// FILE: Sources/App/Extensions/Svg+MenuIcon.swift

import SwiftSvg

extension Svg {

static func menuIcon() -> Svg {


Svg {
Line(x1: 3, y1: 12, x2: 21, y2: 12)
Line(x1: 3, y1: 6, x2: 21, y2: 6)
Line(x1: 3, y1: 18, x2: 21, y2: 18)
}
.width(24)
.height(24)
.viewBox(minX: 0, minY: 0, width: 24, height: 24)
.fill("none")
.stroke("currentColor")
.strokeWidth(2)
.strokeLinecap("round")
.strokeLinejoin("round")
}
}

Next, we should add a new admin link to the index template because after we've created the
admin module we should be able to visit the dashboard from the web frontend, but only if
the user is already authenticated.

/// FILE: Sources/App/Modules/Web/Templates/Html/WebIndexTemplate.swift

import Vapor
import SwiftHtml
import SwiftSvg

public struct WebIndexTemplate: TemplateRepresentable {

public var context: WebIndexContext


var body: Tag

public init(
_ context: WebIndexContext,
@TagBuilder _ builder: () -> Tag
) {
self.context = context
self.body = builder()
}

@TagBuilder
public func render(
_ req: Request
) -> Tag {
Html {
Head {
Meta()
.charset("utf-8")
Meta()
.name(.viewport)
.content("width=device-width, initial-scale=1")

Link(rel: .shortcutIcon)
.href("/images/favicon.ico")
.type("image/x-icon")
Link(rel: .stylesheet)
.href("https://cdn.jsdelivr.net/gh/feathercms/[email protected]
beta.44/feather.min.css")
Link(rel: .stylesheet)
.href("/css/web.css")

Title(context.title)
}
Body {

125
Header {
Div {
A {
Img(src: "/img/logo.png", alt: "Logo")
}
.id("site-logo")
.href("/")

Nav {
Input()
.type(.checkbox)
.id("primary-menu-button")
.name("menu-button")
.class("menu-button")
Label {
Svg.menuIcon()
}
.for("primary-menu-button")
Div {
A("Home")
.href("/")
.class("selected", req.url.path == "/")
A("Blog")
.href("/blog/")
.class("selected", req.url.path == "/blog/")
A("About")
.href("#")
.onClick("javascript:about();")

if req.auth.has(AuthenticatedUser.self) {
A("Admin")
.href("/admin/")

A("Sign out")
.href("/sign-out/")
}
else {
A("Sign in")
.href("/sign-in/")
}
}
.class("menu-items")
}
.id("primary-menu")
}
.id("navigation")
}

Main {
body
}

Footer {
Section {
P {
Text("This site is powered by ")
A("Swift")
.href("https://swift.org")
.target(.blank)
Text(" & ")
A("Vapor")
.href("https://vapor.codes")
.target(.blank)
Text(".")
}
P("myPage &copy; 2020-2022")
}
}

Script()
.type(.javascript)
.src("/js/web.js")

126
}
.lang("en-US")
}
}

The admin module, just like the web module, is the main layout frame for other modules.
They provide a base layout template and other modules can hook into these containers. For
example, the web module has an index template that's used by all the pages on the web
frontend, such as the blog or login screens. The admin module will provide a similar index
template for the admin pages.

As a starting point, we're going to need context before we can create the admin index
template.

/// FILE: Sources/App/Modules/Admin/Templates/Contexts/AdminIndexContext.swift

public struct AdminIndexContext {


public let title: String

public init(
title: String
) {
self.title = title
}
}

Create a new AdminIndexTemplate inside the templates folder with the following contents:

/// FILE: Sources/App/Modules/Admin/Templates/Html/AdminIndexTemplate.swift

import Vapor
import SwiftHtml
import SwiftSvg

public struct AdminIndexTemplate: TemplateRepresentable {

public var context: AdminIndexContext


var body: Tag

public init(
_ context: AdminIndexContext,
@TagBuilder _ builder: () -> Tag
) {
self.context = context
self.body = builder()
}

@TagBuilder
public func render(
_ req: Request
) -> Tag {
Html {
Head {
Meta()
.charset("utf-8")
Meta()
.name(.viewport)
.content("width=device-width, initial-scale=1")
Meta()
.name("robots")
.content("noindex")
Link(rel: .shortcutIcon)
.href("/images/favicon.ico")
.type("image/x-icon")
Link(rel: .stylesheet)

127
.href("https://cdn.jsdelivr.net/gh/feathercms/[email protected]
beta.44/feather.min.css")
Link(rel: .stylesheet)
.href("/css/admin.css")

Title(context.title)
}
Body {
Div {
A {
Img(src: "/img/logo.png", alt: "Logo")
.title("Logo")
.style("width: 300px")
}
.href("/")

Nav {
Input()
.type(.checkbox)
.id("secondary-menu-button")
.name("menu-button")
.class("menu-button")
Label {
Svg.menuIcon()
}
.for("secondary-menu-button")

Div {
A("Sign out")
.href("/sign-out/")
}
.class("menu-items")
}
.id("secondary-menu")
}
.id("navigation")

Main {
body
}

Script()
.type(.javascript)
.src("/js/admin.js")

}
}
.lang("en-US")
}
}

The main admin template is just slightly di erent from the web index. The very rst change is
that there's a new meta tag for robots since we don't want admin pages to be indexed. Not
like any robot could access these, since we'll guard them with a middleware, so it's not going
to be available publicly anyway, but let's just add the robots meta anyway.

We're linking the Feather CSS framework here as well since it's a generic shared CSS le
with very common stu . We also included a admin.css stylesheet that's going to contain the
admin-speci c styles. The menu structure is di erent from the web and we added an
admin.js le we'll use at the end.

We'll also need something like a home screen for the content management system. We're
going to call this a dashboard and as usual, we need a context for it.

/// FILE: Sources/App/Modules/Admin/Templates/Contexts/AdminDashboardContext.swift

128
fi
fi
ff
ff
ff
fi
fi
struct AdminDashboardContext {
let icon: String
let title: String
let message: String
}

Let's add a new AdminDashboardTemplate right next to the index le in the templates
folder.

/// FILE: Sources/App/Modules/Admin/Templates/Html/AdminDashboardTemplate.swift

import Vapor
import SwiftHtml

struct AdminDashboardTemplate: TemplateRepresentable {

var context: AdminDashboardContext

init(
_ context: AdminDashboardContext
) {
self.context = context
}

@TagBuilder
func render(
_ req: Request
) -> Tag {
AdminIndexTemplate(
.init(title: context.title)
) {
Div {
Section {
P(context.icon)
H1(context.title)
P(context.message)
}
}
.id("dashboard")
.class("container")
}
.render(req)
}
}

Now create a new AdminFrontendController that can render the dashboard screen for the
CMS.

/// FILE: Sources/App/Modules/Admin/Controllers/AdminFrontendController.swift

import Vapor

struct AdminFrontendController {

func dashboardView(
req: Request
) throws -> Response {
let user = try req.auth.require(AuthenticatedUser.self)
let template = AdminDashboardTemplate(
.init(
icon: "👋 ",
title: "Dashboard",
message: "Hello \(user.email), welcome to the CMS."
)
)
return req.templates.renderHtml(template)

129
fi
}
}

Hook up this admin controller by creating a new AdminRouter object. If you remember
we've enabled the session authenticator middleware for all the routes, so users will be
automatically authenticated if there's a valid session.

We can use the redirectMiddleware function on an Authenticatable type which will return a
middleware that redirects every unauthenticated tra c to a speci ed path.

/// FILE: Sources/App/Modules/Admin/AdminRouter.swift

import Vapor

struct AdminRouter: RouteCollection {

let controller = AdminFrontendController()

func boot(routes: RoutesBuilder) throws {


routes
.grouped(
AuthenticatedUser.redirectMiddleware(
path: "/sign-in/"
)
)
.get("admin", use: controller.dashboardView)
}
}

As I mentioned before, admin views will be only available for authenticated users; this way
we can guard our admin routes from unauthorized public access.

This middleware checks if there's an existing AuthenticatedUser object in the req.auth


storage: if there's one, then Vapor will call the request handler as usual; otherwise, it'll
perform an HTTP redirect to the provided path. You could also make an endpoint protected
by calling the try req.auth.require(AuthenticatedUser.self) function inside the request
handler, but using middleware is a bit more elegant.

To nish up this module we should create a new AdminModule struct inside the Admin
folder and boot the admin router instance using the route.

/// FILE: Sources/App/Modules/Admin/AdminModule.swift

import Vapor

struct AdminModule: ModuleInterface {

let router = AdminRouter()

func boot(_ app: Application) throws {


try router.boot(routes: app.routes)
}
}

We'll have to register this new module inside the con guration le to make things work.

/// FILE: Sources/App/con gure.swift

import Vapor

130
fi
fi
ffi
fi
fi
fi
import Fluent
import FluentSQLiteDriver
import Liquid
import LiquidLocalDriver

public func configure(


_ app: Application
) throws {

// ...

let modules: [ModuleInterface] = [


WebModule(),
UserModule(),
AdminModule(),
BlogModule(),
]
for module in modules {
try module.boot(app)
}

try app.autoMigrate().wait()
}

Run the app, sign in with the default user account and click on the admin menu. Now we
have the bare bones of our CMS. These steps should be very familiar by now; nally, we're
ready to move forward and build some real content management screens.

LIST
We're going to create a new admin list component for the blog module so we can have a
nice list for all the existing blog posts. As usual, we start with a context for the posts.

/// FILE: Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminListContext.swift

struct BlogPostAdminListContext {
let title: String
let list: [Blog.Post.List]
}

Create a new template le called BlogPostAdminListTemplate with the following contents.

/// FILE: Sources/App/Modules/Blog/Templates/Html/BlogPostAdminListTemplate.swift

import Vapor
import SwiftHtml

struct BlogPostAdminListTemplate: TemplateRepresentable {

var context: BlogPostAdminListContext

init(
_ context: BlogPostAdminListContext
) {
self.context = context
}

@TagBuilder
func render(
_ req: Request
) -> Tag {
AdminIndexTemplate(

131
fi
fi
.init(title: context.title)
) {
Div {
Section {
H1(context.title)
}
.class("lead")

Table {
Thead {
Tr {
Th("Image")
Th("Title")
Th("Preview")
}
}
Tbody {
for item in context.list {
Tr {
Td {
Img(src: item.image, alt: item.title)
}
Td(item.title)
Td {
A("Preview")
.href("/" + item.slug + "/")
}
}
}
}
}
}
.id("list")
}
.render(req)
}
}

Inside this template, we simply use the context list array to render a table based on the blog
post list-objects. We can simply display the image and the title of the post and a preview URL
that goes to the post page. We can use the built-in SwiftHtml tags to render our HTML table.

Next, before we move forward with the controller, we should clean up some code that we
left untouched since we've created the BlogPostModel type. Map functions are annoying,
but since we don't want to use the database models, because they might contain sensitive
data, we need a place for the mapper functions. It's a good idea to create a
BlogPostApiController and place the map list function there to convert a blog post model
into a public Blog.Post.List.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogPostApiController.swift

import Vapor

struct BlogPostApiController {

func mapList(_ model: BlogPostModel) -> Blog.Post.List {


.init(
id: model.id!,
title: model.title,
slug: model.slug,
image: model.imageKey,
excerpt: model.excerpt,
date: model.date
)
}
}

132
Now we can make a new controller that's going to be responsible for rendering post-related
admin views. Let's make a new BlogPostAdminController with a listView function to query
all the available entities, and map them using the new API controller; nally we render the
template.

/// FILE: Sources/App/Modules/Controllers/BlogPostAdminController.swift

import Vapor

struct BlogPostAdminController {

func listView(_ req: Request) async throws -> Response {


let posts = try await BlogPostModel.query(on: req.db).all()
let api = BlogPostApiController()
let list = posts.map { api.mapList($0) }
let template = BlogPostAdminListTemplate(
.init(
title: "Posts",
list: list
)
)
return req.templates.renderHtml(template)
}
}

Inside the BlogFrontendController we can also replace the old map logic with the new API
method; this way we'll have less duplicated code inside our codebase.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogFrontendController.swift

import Vapor
import Fluent

struct BlogFrontendController {

func blogView(req: Request) async throws -> Response {


let posts = try await BlogPostModel
.query(on: req.db)
.sort(\.$date, .descending)
.all()

let api = BlogPostApiController()


let list = posts.map { api.mapList($0) }

let ctx = BlogPostsContext(


icon: "🔥 ",
title: "Blog",
message: "Hot news and stories about everything.",
posts: list
)

return req.templates.renderHtml(BlogPostsTemplate(ctx))
}

// ...
}

We need to use the redirectMiddleware method once again in the router since we don't
want to allow guests to visit the blog post list admin page. We can also use the grouped
method on the routes to group a route by an array of path components.

/// FILE: Sources/App/Modules/Blog/BlogRouter.swift

import Vapor

133
fi
struct BlogRouter: RouteCollection {

let controller = BlogFrontendController()


let postAdminController = BlogPostAdminController()

func boot(routes: RoutesBuilder) throws {


routes.get("blog", use: controller.blogView)
routes.get(.anything, use: controller.postView)

routes
.grouped(AuthenticatedUser.redirectMiddleware(path: "/"))
.grouped("admin", "blog")
.get("posts", use: postAdminController.listView)
}
}

Now in the admin dashboard template, we'll add a new link to access the blog posts.

/// FILE: Sources/App/Modules/Blog/Templates/Html/AdminDashboardTemplate.swift

import Vapor
import SwiftHtml

struct AdminDashboardTemplate: TemplateRepresentable {

var context: AdminDashboardContext

init(
_ context: AdminDashboardContext
) {
self.context = context
}

@TagBuilder
func render(
_ req: Request
) -> Tag {
AdminIndexTemplate(
.init(title: context.title)
) {
Div {
Section {
P(context.icon)
H1(context.title)
P(context.message)
}

Nav {
H2("Blog")
Ul {
Li {
A("Posts")
.href("/admin/blog/posts/")
}
}
}
}
.id("dashboard")
.class("container")
}
.render(req)
}
}

We're going to insert some additional CSS to make the list a bit nicer. Paste the following
snippet into the admin.css le.

/* FILE: Public/css/admin.css */

134
fi
tr {
column-gap: 1rem;
}

That's how you can integrate a new component into the admin interface. Run the app and
check the newly created list. It should show you all the available blog posts.

DETAIL
The detail view for a post is going to be very similar, but we're also going to learn some new
things while we build this functionality. First, we start with the detail context.

/// FILE: Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminDetailContext.swift

struct BlogPostAdminDetailContext {
let title: String
let detail: Blog.Post.Detail
}

We're going to use the Dl, Dt, and Dd elements in the corresponding template to build up
our detail view. Since blog posts have a date eld we're also going to need a date formatter.

/// FILE: Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminDetailContext.swift

import Vapor
import SwiftHtml

struct BlogPostAdminDetailTemplate: TemplateRepresentable {

var context: BlogPostAdminDetailContext

init(
_ context: BlogPostAdminDetailContext
) {
self.context = context
}

var dateFormatter: DateFormatter = {


let formatter = DateFormatter()
formatter.dateStyle = .long
formatter.timeStyle = .short
return formatter
}()

@TagBuilder
func render(
_ req: Request
) -> Tag {
AdminIndexTemplate(
.init(title: context.title)
) {
Div {
Section {
H1(context.title)
}
.class("lead")

Dl {
Dt("Image")
Dd {
Img(

135
fi
src: context.detail.image,
alt: context.detail.title
)
}

Dt("Title")
Dd(context.detail.title)

Dt("Excerpt")
Dd(context.detail.excerpt)

Dt("Date")
Dd(dateFormatter.string(from: context.detail.date))

Dt("Content")
Dd(context.detail.content)
}
}
.id("detail")
.class("container")
}
.render(req)
}
}

We should also extend the BlogPostApiController with a new mapDetail function; this will
allow us to map the fetched model into a detail object. Later on, we're going to use these
kinds of API controllers to return JSON responses through the API layer.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogPostApiController.swift

import Vapor

struct BlogPostApiController {

func mapList(_ model: BlogPostModel) -> Blog.Post.List {


.init(
id: model.id!,
title: model.title,
slug: model.slug,
image: model.imageKey,
excerpt: model.excerpt,
date: model.date
)
}

func mapDetail(_ model: BlogPostModel) -> Blog.Post.Detail {


.init(
id: model.id!,
title: model.title,
slug: model.slug,
image: model.imageKey,
excerpt: model.excerpt,
date: model.date,
category: .init(
id: model.category.id!,
title: model.category.title
),
content: model.content
)
}
}

Inside the BlogPostAdminController we have to nd the current blog post model somehow.
Since we're going to use the postId parameter inside the path when we register our route
handler we can get back the id value as a string by calling the req.parameters.get() method.

136
fi
It's really easy to turn the string into a UUID object and use that to query our database
model.

The detailView method is now very straightforward: we simply nd the model, transform the
model into a proper detail object, then render the template using the context.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogPostAdminController.swift

import Vapor
import Fluent

struct BlogPostAdminController {

func find(_ req: Request) async throws -> BlogPostModel {


guard
let id = req.parameters.get("postId"),
let uuid = UUID(uuidString: id),
let post = try await BlogPostModel
.query(on: req.db)
.filter(\.$id == uuid)
.with(\.$category)
.first()
else {
throw Abort(.notFound)
}
return post
}

func listView(_ req: Request) async throws -> Response {


let posts = try await BlogPostModel.query(on: req.db).all()
let api = BlogPostApiController()
let list = posts.map { api.mapList($0) }
let template = BlogPostAdminListTemplate(
.init(
title: "Posts",
list: list
)
)
return req.templates.renderHtml(template)
}

func detailView(_ req: Request) async throws -> Response {


let post = try await find(req)
let detail = BlogPostApiController().mapDetail(post)
let template = BlogPostAdminDetailTemplate(
.init(
title: "Post details",
detail: detail
)
)
return req.templates.renderHtml(template)
}
}

We can refactor one more thing inside the blog frontend controller. We can use the same API
object to map the details of a blog post after we fetch the model in the postView function.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogFrontendController.swift

import Vapor
import Fluent

struct BlogFrontendController {

func blogView(req: Request) async throws -> Response {


let posts = try await BlogPostModel
.query(on: req.db)

137
fi
.sort(\.$date, .descending)
.all()

let api = BlogPostApiController()


let list = posts.map { api.mapList($0) }

let ctx = BlogPostsContext(


icon: "🔥 ",
title: "Blog",
message: "Hot news and stories about everything.",
posts: list
)

return req.templates.renderHtml(BlogPostsTemplate(ctx))
}

func postView(req: Request) async throws -> Response {


let slug = req.url.path.trimmingCharacters(
in: .init(charactersIn: "/")
)
guard
let post = try await BlogPostModel
.query(on: req.db)
.filter(\.$slug == slug)
.with(\.$category)
.first()
else {
return req.redirect(to: "/")
}
let api = BlogPostApiController()
let ctx = BlogPostContext(post: api.mapDetail(post))
return req.templates.renderHtml(BlogPostTemplate(ctx))
}
}

It's now time to register our route handlers. We can store the post's endpoint in a variable, so
later on we can reuse it and we don't have to group everything all over again.

When registering a route parameter, you should pre x it with a ":" so Vapor will know that it's
not a static path component, but a dynamic route parameter. You can query back this route
parameter later on by referencing its name.

/// FILE: Sources/App/Modules/Blog/BlogRouter.swift

import Vapor

struct BlogRouter: RouteCollection {

let controller = BlogFrontendController()


let postAdminController = BlogPostAdminController()

func boot(routes: RoutesBuilder) throws {


routes.get("blog", use: controller.blogView)
routes.get(.anything, use: controller.postView)

let posts = routes


.grouped(AuthenticatedUser.redirectMiddleware(path: "/"))
.grouped("admin", "blog", "posts")

posts.get(use: postAdminController.listView)
posts.get(":postId", use: postAdminController.detailView)
}
}

138
fi
Let's make one more little change inside the post list template. We should add a hyperlink to
the title eld so that when the user clicks it'll open the post detail page.

/// FILE: Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminListContext.swift

import Vapor
import SwiftHtml

struct BlogPostAdminListTemplate: TemplateRepresentable {

var context: BlogPostAdminListContext

init(
_ context: BlogPostAdminListContext
) {
self.context = context
}

@TagBuilder
func render(
_ req: Request
) -> Tag {
AdminIndexTemplate(
.init(title: context.title)
) {
Div {
Section {
H1(context.title)
}
.class("lead")

Table {
Thead {
Tr {
Th("Image")
Th("Title")
Th("Preview")
}
}
Tbody {
for item in context.list {
Tr {
Td {
Img(src: item.image, alt: item.title)
}
Td {
A(item.title)
.href("/admin/blog/posts/" + item.id.uuidString +
"/")
}
Td {
A("Preview")
.href("/" + item.slug + "/")
}
}
}
}
}
}
.id("list")
}
.render(req)
}
}

That's how we can render the post details; now if you build and run the app you should be
able to navigate to the detail page and see more information about a blog post.

139
fi
CREATE
The next step should be the ability to create new blog posts. We're going to build an edit
form for this purpose using our abstract form component and form elds.

The BlogPostEditForm is going to be a class, the init method takes a BlogPostModel


instance, which we'll store as an unowned reference. We can check if a Fluent model is
already persisted or not via the model.$id.value property wrapper, so we set up the form
submit URL based on that.

We simply store a reference of our BlogPostModel inside the form and inside the read
function, we set the right value of the given property as the output value. The write function
does the exact opposite, it'll turn the input values into model properties. In other words, we
read the model data into the form and we write the date of the form into the model.

Since we're working with reference types, this time we have to be careful with strong
references, so that's why we're passing locally referenced objects as unowned pointers for
the blocks. This is a bit inconvenient for now, but we're going to x it later on.

/// FILE: Sources/App/Modules/Blog/Forms/BlogPostEditForm.swift

import Vapor

final class BlogPostEditForm: AbstractForm {

unowned var model: BlogPostModel

var dateFormatter: DateFormatter = {


let formatter = DateFormatter()
formatter.dateStyle = .long
formatter.timeStyle = .short
return formatter
}()

public init(_ model: BlogPostModel) {


var url = "/admin/blog/posts/"
if let id = model.$id.value {
url = url + id.uuidString + "/update/"
}
else {
url = url + "create/"
}
self.model = model
super.init(
action: .init(
method: .post,
url: url,
enctype: .multipart
)
)
self.fields = createFields()
}

@FormComponentBuilder
func createFields() -> [FormComponent] {
ImageField("image", path: "blog/post")
.read { [unowned self] in
$1.output.context.previewUrl = model.imageKey
($1 as! ImageField).imageKey = model.imageKey
}
.write { [unowned self] in
model.imageKey = ($1 as! ImageField).imageKey ?? ""

140
fi
fi
}

InputField("slug")
.config {
$0.output.context.label.required = true
}
.validators {
FormFieldValidator.required($1)
}
.read { [unowned self] in
$1.output.context.value = model.slug
}
.write { [unowned self] in
model.slug = $1.input
}

InputField("title")
.config {
$0.output.context.label.required = true
}
.validators {
FormFieldValidator.required($1)
}
.read { [unowned self] in
$1.output.context.value = model.title
}
.write { [unowned self] in
model.title = $1.input
}

InputField("date")
.config {
$0.output.context.label.required = true
$0.output.context.value = dateFormatter.string(from: Date())
}
.validators {
FormFieldValidator.required($1)
}
.read { [unowned self] in
$1.output.context.value = dateFormatter.string(from: model.date)
}
.write { [unowned self] in
model.date = dateFormatter.date(from: $1.input) ?? Date()
}

TextareaField("excerpt")
.read { [unowned self] in
$1.output.context.value = model.excerpt
}
.write { [unowned self] in
model.excerpt = $1.input
}

TextareaField("content")
.read { [unowned self] in
$1.output.context.value = model.content
}
.write { [unowned self] in
model.content = $1.input
}

SelectField("category")
.load { req, field in
let categories = try await BlogCategoryModel
.query(on: req.db)
.all()
field.output.context.options = categories.map {
OptionContext(key: $0.id!.uuidString, label: $0.title)
}
}
.read { [unowned self] req, field in
field.output.context.value = model.$category.id.uuidString
}
.write { [unowned self] req, field in

141
if
let uuid = UUID(uuidString: field.input),
let category = try await BlogCategoryModel
.find(uuid, on: req.db)
{
model.$category.id = category.id!
}
}
}
}

The select category eld is a bit more special, in the load method we fetch the available
categories from the database and set the option values based on the result. The write
function will turn the selected category id string into a UUID type and we check if there's an
existing category with that identi er or not.

The next step is to create a template le for our edit form. We're going to reuse this edit form
both for the create and update actions. Let's make a BlogPostAdminEditContext for the
view.

/// FILE: Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminEditContext.swift

struct BlogPostAdminEditContext {
let title: String
let form: TemplateRepresentable
}

The BlogPostAdminEditTemplate is going to be very simple, we just render the edit form as
it is.

/// FILE: Sources/App/Modules/Blog/Templates/Html/BlogPostAdminEditTemplate.swift

import Vapor
import SwiftHtml

struct BlogPostAdminEditTemplate: TemplateRepresentable {

var context: BlogPostAdminEditContext

init(
_ context: BlogPostAdminEditContext
) {
self.context = context
}

@TagBuilder
func render(
_ req: Request
) -> Tag {
AdminIndexTemplate(
.init(title: context.title)
) {
Div {
Section {
H1(context.title)
}
.class("lead")

context.form.render(req)
}
.id("edit")
.class("container")
}
.render(req)

142
fi
fi
fi
}
}

Inside the controller, we should be able to use the form to create a new blog post.

We call the form events more or less in the same order as it was described in Chapter 7.

In the createView, we initialize an empty model and a form using that model. We just call the
load function so the form can load the category relations and that's it: we're ready to render.

The createAction method is a bit more complicated, but rst, we need a new model and a
form. After that, we call the load method and we process the input elds. We also have to
validate the input, but if something went wrong we can render the edit form with the errors.
Otherwise, we continue with the work ow and call the write method, this will ensure that our
model is populated with the validated input values.

Finally, we call the model.create(on:) method, this will persist the entity into the database
and we also call the save function on the form, so if there's an additional save operation
that'll be performed as well. As a very last step, we redirect the user to the detail page.

/// FILE: Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminListContext.swift

import Vapor
import Fluent

struct BlogPostAdminController {

// ...

private func renderEditForm(


_ req: Request,
_ title: String,
_ form: BlogPostEditForm
) -> Response {
let template = BlogPostAdminEditTemplate(
.init(
title: title,
form: form.render(req: req)
)
)
return req.templates.renderHtml(template)
}

func createView(
_ req: Request
) async throws -> Response {
let model = BlogPostModel()
let form = BlogPostEditForm(model)
try await form.load(req: req)
return renderEditForm(req, "Create post", form)
}

func createAction(
_ req: Request
) async throws -> Response {
let model = BlogPostModel()
let form = BlogPostEditForm(model)
try await form.load(req: req)
try await form.process(req: req)
let isValid = try await form.validate(req: req)
guard isValid else {
return renderEditForm(req, "Create post", form)
}
try await form.write(req: req)

143
fl
fi
fi
try await model.create(on: req.db)
try await form.save(req: req)
return req.redirect(
to: "/admin/blog/posts/\(model.id!.uuidString)/"
)
}
}

Of course, we have to register two new create routes to make the handlers work.

/// FILE: Sources/App/Modules/Blog/BlogRouter.swift

import Vapor

struct BlogRouter: RouteCollection {

let controller = BlogFrontendController()


let postAdminController = BlogPostAdminController()

func boot(routes: RoutesBuilder) throws {


routes.get("blog", use: controller.blogView)
routes.get(.anything, use: controller.postView)

let posts = routes


.grouped(AuthenticatedUser.redirectMiddleware(path: "/"))
.grouped("admin", "blog", "posts")

posts.get(use: postAdminController.listView)
posts.get(":postId", use: postAdminController.detailView)

posts.get("create", use: postAdminController.createView)


posts.post("create", use: postAdminController.createAction)
}
}

Now you can try out what we've just made by entering the /admin/blog/posts/create/ URL.
In this chapter, we won't care too much about navigation links, because the next chapter will
feature a more generic solution.

Note: when you create a new post you should be able to upload the image, but it won't be
displayed on the detail screen just yet. We will resolve this issue in the upcoming chapters.

UPDATE
Reusability is a very good thing. In our case creating a new post is done through the same
form as the update action happens. We can reuse the BlogPostEditForm to support both
functionalities by adding some really simple minor changes to the controller.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogPostAdminController.swift

import Vapor
import Fluent

struct BlogPostAdminController {

// ...

func updateView(_ req: Request) async throws -> Response {


let model = try await find(req)
let form = BlogPostEditForm(model)

144
try await form.load(req: req)
try await form.read(req: req)
return renderEditForm(req, "Update post", form)
}

func updateAction(_ req: Request) async throws -> Response {


let model = try await find(req)
let form = BlogPostEditForm(model)
try await form.load(req: req)
try await form.process(req: req)
let isValid = try await form.validate(req: req)
guard isValid else {
return renderEditForm(req, "Update post", form)
}
try await form.write(req: req)
try await model.update(on: req.db)
try await form.save(req: req)
return req.redirect(
to: "/admin/blog/posts/\(model.id!.uuidString)/update/"
)
}
}

We're going to use a URL parameter to look up a post; fortunately, we already have a nd
function. After we look up the model, we have to load the form and we also want to read
back the model values into the form elds.

The updateAction method more or less follows the same principles as the create action. The
main di erence is that instead of creating a new model we nd the existing one based on
the parameter. We've also changed the title of the page and we call the update method on
the model instead of the create. Finally, we redirect to the same update URL instead of
showing the details.

Don't forget to register the new routes for the controller functions.

/// FILE: Sources/App/Modules/Blog/BlogRouter.swift

import Vapor

struct BlogRouter: RouteCollection {

let controller = BlogFrontendController()


let postAdminController = BlogPostAdminController()

func boot(routes: RoutesBuilder) throws {


routes.get("blog", use: controller.blogView)
routes.get(.anything, use: controller.postView)

let posts = routes


.grouped(AuthenticatedUser.redirectMiddleware(path: "/"))
.grouped("admin", "blog", "posts")

posts.get(use: postAdminController.listView)

let postId = posts.grouped(":postId")

postId.get(use: postAdminController.detailView)

posts.get("create", use: postAdminController.createView)


posts.post("create", use: postAdminController.createAction)

postId.get("update", use: postAdminController.updateView)


postId.post("update", use: postAdminController.updateAction)
}
}

145
ff
fi
fi
fi
We can group the posts by the :postId parameter and use that as a base route when we
register our detail and update handlers. Feel free to try this new edit functionality now.

DELETE
The very last thing that we're going to implement in this chapter is a basic delete
functionality. We're going to use a simple template with a delete form to display a
con rmation screen before we remove the record from the database.

The context is going to feature a name and type property, this way we can tell the user more
information about the entity.

/// FILE: Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminDeleteContext.swift

struct BlogPostAdminDeleteContext {
let title: String
let name: String
let type: String
}

Based on the delete context we can render our template by con guring a simple form with a
post action to the delete URL. It's only going to contain a submit button and a link to cancel
the delete action.

/// FILE: Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminDeleteTemplate.swift

import Vapor
import SwiftHtml

struct BlogPostAdminDeleteTemplate: TemplateRepresentable {

var context: BlogPostAdminDeleteContext

init(
_ context: BlogPostAdminDeleteContext
) {
self.context = context
}

@TagBuilder
func render(
_ req: Request
) -> Tag {
AdminIndexTemplate(
.init(title: context.title)
) {
Div {
Span("🗑 ")
.class("icon")
H1(context.title)
P("You are about to permanently delete the<br>`\(context.name)` \
(context.type).")

Form {
Input()
.type(.submit)
.class(["button", "destructive"])
.style("display: inline")
.value("Delete")

146
fi
fi
A("Cancel")
.href("/admin/blog/posts/")
.class(["button", "cancel"])
}
.method(.post)
.id("delete-form")
}
.class(["lead", "container", "center"])
}
.render(req)
}
}

We're going to render this view using a get endpoint and handle the post delete request
using a di erent deleteAction route handler. We can set up these functions using the admin
controller.

In both methods, we try to nd the model, and inside the action handler, we can remove the
associated blog post image and the model itself by using the model.delete(on:) function.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogPostAdminController.swift

import Vapor
import Fluent

struct BlogPostAdminController {

// ...

func deleteView(_ req: Request) async throws -> Response {


let model = try await find(req)

let template = BlogPostAdminDeleteTemplate(


.init(
title: "Delete post",
name: model.title,
type: "post"
)
)
return req.templates.renderHtml(template)
}

func deleteAction(_ req: Request) async throws -> Response {


let model = try await find(req)
try await req.fs.delete(key: model.imageKey)
try await model.delete(on: req.db)
return req.redirect(to: "/admin/blog/posts/")
}
}

As usual, we have to register the routes for these delete endpoint handlers.

/// FILE: Sources/App/Modules/Blog/BlogRouter.swift

import Vapor

struct BlogRouter: RouteCollection {

let controller = BlogFrontendController()


let postAdminController = BlogPostAdminController()

func boot(routes: RoutesBuilder) throws {


routes.get("blog", use: controller.blogView)
routes.get(.anything, use: controller.postView)

let posts = routes

147
ff
fi
.grouped(AuthenticatedUser.redirectMiddleware(path: "/"))
.grouped("admin", "blog", "posts")

posts.get(use: postAdminController.listView)

let postId = posts.grouped(":postId")

postId.get(use: postAdminController.detailView)

posts.get("create", use: postAdminController.createView)


posts.post("create", use: postAdminController.createAction)

postId.get("update", use: postAdminController.updateView)


postId.post("update", use: postAdminController.updateAction)

postId.get("delete", use: postAdminController.deleteView)


postId.post("delete", use: postAdminController.deleteAction)
}
}

That's it, if you visit a detail page and you append the delete word to the end of the route
you should see a con rmation page with the ability to delete blog posts.

SUMMARY
This chapter was all about building a Content Management System with web-based CRUD
support using Vapor. As you can see, the admin module provides a nice frame around these
functionalities. We've also learned how to make reusable form components and elds for
both the create and update endpoints. Finally, we've learned how to delete records from
persistent storage. Using Fluent is quite simple when it comes to data manipulation; there
are available methods that you can call directly on the model for almost anything.

148
fi
fi
CHAPTER 10
BUILDING A GENERIC ADMIN INTERFACE

This chapter is about turning our basic CMS into a generic solution. By leveraging the power
of Swift protocols we're going to be able to come up with several base controllers that can
be used to manage database models through the admin interface. This methodology allows
us to easily de ne, list, create, update, and delete controllers. By the end of this chapter,
we're going to have a completely working admin solution for the blog module.

GENERIC TABLE TEMPLATE SYSTEM


Let’s start by creating the new folders we’ll use during this chapter. You can use the
commands below to add them:

cd ~/myProject
mkdir -p Sources/App/Framework/Controllers
mkdir -p Sources/App/Modules/Blog/Editors

First of all, we'll need some new context objects and templates. Let's move the Templates
folder out of the Form directory and create a new LinkContext struct. You can use the
command below to move it:

mv Sources/App/Framework/Form/Templates \
Sources/App/Framework/Templates

Create the LinkContext struct:

/// FILE: Sources/App/Framework/Templates/Context/LinkContext.swift

import Vapor

public struct LinkContext {

public let label: String


public let path: String
public let absolute: Bool
public let isBlank: Bool
public let dropLast: Int

public init(
label: String,
path: String = "",
absolute: Bool = false,
isBlank: Bool = false,
dropLast: Int = 0
) {
self.label = label
self.path = path
self.absolute = absolute
self.isBlank = isBlank
self.dropLast = dropLast
}

149
fi
public func url(
_ req: Request,
_ infix: [PathComponent] = []
) -> String {
if absolute {
return path
}
return "/" +
(req.url.path.pathComponents.dropLast(dropLast) +
(infix + path.pathComponents)).string
}
}

This new item will help us to deal with navigation links. The path is a relative path value by
default, but you can store an absolute URL inside this variable if you set the absolute
property to true. The dropLast variable indicates how many path components need to be
dropped before we append the path variable to the URL.

By introducing an in x PathComponent argument we can easily insert other path


components between the reduced base URL and the path su x. This is going to be useful
when we display table row actions and we have to inject the model identi er before the
action path component.

We're also going to need a LinkTemplate to render the links.

/// FILE: Sources/App/Framework/Templates/Html/LinkTemplate.swift

import Vapor
import SwiftHtml

public struct LinkTemplate: TemplateRepresentable {

var context: LinkContext


var body: Tag
var pathInfix: String?

public init(
_ context: LinkContext,
pathInfix: String? = nil,
_ builder: ((String) -> Tag)? = nil
) {
self.context = context
self.pathInfix = pathInfix
self.body = builder?(context.label) ?? Text(context.label)
}

@TagBuilder
public func render(
_ req: Request
) -> Tag {
A { body }
.href(context.url(req, pathInfix?.pathComponents ?? []))
.target(.blank, context.isBlank)
}
}

This is how you can construct a link and how you can use the in x argument in practice.

/// Dropping path components


/// current URL is /admin/blog/posts/[uuid]/update/
let link = LinkContext(label: "Create", path: "create", dropLast: 2)
/// drop last 2 segments, remains: /admin/blog/posts/
/// nal URL will be: /admin/blog/posts/create/

150
fi
fi
ffi
fi
fi
/// Using path in x and custom Tag
struct Row {
let id: String
let image: String
}
let row = Row(id: "1", image: "http://localhost/example.jpg")
let link = LinkContext(label: "Update", path: "update")
// current URL is /admin/blog/posts/
let template = LinkTemplate(link, pathInfix: row.id) { label in
Img(src: row.image, alt: label)
}
// template.render(req) -> A { Img(...) }.href(url)
// final URL will be: /admin/blog/posts/1/update/

This way we're able to generate a new URL with a given path su x from the current URL by
removing the last two components and appending the new path to the end. In the second
example, we were able to easily insert the row identi er and use a custom HTML image
element to display the link.

To come up with a generic table view that can be used to render all kinds of data, the very
rst component we need is a cell context. As a beginning, this context is going to be able to
display raw textual data and images. It's also going to be possible to place a link on a given
cell, we can use the LinkContext for this purpose.

/// FILE: Sources/App/Framework/Templates/Context/CellContext.swift

public struct CellContext {

public enum `Type`: String {


case text
case image
}

public let value: String


public let link: LinkContext?
public let type: `Type`

public init(
_ value: String,
link: LinkContext? = nil,
type: `Type` = .text
) {
self.type = type
self.value = value
self.link = link
}
}

Inside the render method of the CellTemplate we simply switch the cell context type and if
we had a link value we display the raw text or image content inside a link, otherwise we just
return the proper HTML tag without a link wrapper.

/// FILE: Sources/App/Framework/Templates/Templates/CellTemplate.swift

import Vapor
import SwiftHtml

public struct CellTemplate: TemplateRepresentable {

var context: CellContext


var rowId: String

public init(
_ context: CellContext,

151
fi
fi
fi
ffi
rowId: String
) {
self.context = context
self.rowId = rowId
}

@TagBuilder
public func render(
_ req: Request
) -> Tag {
Td {
switch context.type {
case .text:
if let link = context.link {
LinkTemplate(link, pathInfix: rowId).render(req)
}
else {
Text(context.value)
}
case .image:
if let link = context.link {
LinkTemplate(link, pathInfix: rowId) { label in
Img(src: context.value, alt: label)
}
.render(req)
}
else {
Img(src: context.value, alt: context.value)
}
}
}
.class("field")
}
}

Table cells are organized by rows, each row has a unique identi er, so let's model this
structure by creating a RowContext object that can contain multiple cell items.

/// FILE: Sources/App/Framework/Templates/Contexts/RowContext.swift

public struct RowContext {

public let id: String


public let cells: [CellContext]

public init(
id: String,
cells: [CellContext]
) {
self.id = id
self.cells = cells
}
}

We should also de ne a context object for table columns. By using a key and a label value,
it's going to be possible to add a custom sorting mechanism to the tables. From now on,
we're going to use the label value to simply display the column names without the sort
option.

/// FILE: Sources/App/Framework/Templates/Contexts/ColumnContext.swift

public struct ColumnContext {

public let key: String


public let label: String

public init(
_ key: String,

152
fi
fi
label: String? = nil
) {
self.key = key
self.label = label ?? key.capitalized
}
}

Now we're ready to compose our table view, based on the structures that we've just created.
One last thing that we should add is an actions array where we can store LinkContext values
and display these actions if needed. That's what our complete TableContext object should
look like.

/// FILE: Sources/App/Framework/Templates/Contexts/TableContext.swift

public struct TableContext {

public let columns: [ColumnContext]


public let rows: [RowContext]
public let actions: [LinkContext]

public init(
columns: [ColumnContext],
rows: [RowContext],
actions: [LinkContext] = []
) {
self.columns = columns
self.rows = rows
self.actions = actions
}
}

Inside the TableTemplate we're going to take advantage of the previously de ned contexts
and templates. We can map both the columns and the actions to a Th tag inside the Thead
section. In the Tbody block we can iterate through the rows and display a cell for each cell
plus an action with a link template for the set of actions. We use the pathIn xs to put the row
identi er (model identi er) into the action link.

/// FILE: Sources/App/Framework/Templates/Html/TableTemplateContext.swift

import Vapor
import SwiftHtml

public struct TableTemplate: TemplateRepresentable {

var context: TableContext

public init(
_ context: TableContext
) {
self.context = context
}

@TagBuilder
public func render(
_ req: Request
) -> Tag {
Table {
Thead {
Tr {
context.columns.map { column in
Th(column.label)
.id(column.key)
.class("field")
}
context.actions.map { action in

153
fi
fi
fi
fi
Th(action.label)
.class("action")
}
}
}
Tbody {
for row in context.rows {
Tr {
row.cells.map { CellTemplate($0, rowId: row.id).render(req) }
context.actions.map { action in
Td {
LinkTemplate(action, pathInfix: row.id).render(req)
}
.class("action")
}
}
.id(row.id)
}
}
}
}
}

That's how we can build up a complex template structure by using smaller elements. This
structure resembles SwiftUI and that's why it's so powerful. Composition is a great way to
express data models and templates like this. Now we're ready to render lists based on table
templates.

GENERIC MODEL AND LIST CONTROLLERS


Now we should talk a bit about controllers. We've created just a few controllers so far, but
this time we're going to invent a special protocol for those who work with an associated
database model type. We should notice that both the BlogPostAdminController uses an
underlying Fluent model to render lists, edit forms and delete content. The upcoming
BlogCategoryAdminController will follow the same pattern, so it makes sense to come up
with a common protocol.

This new protocol is going to be called ModelController and it's going to have an associated
type value, which is a generic placeholder for a type that the protocol implementation will
de ne as a type alias. We can call this DatabaseModel and it should be a
DatabaseModelInterface type this way we can ensure that only Fluent model types can be
used as database models.

We're going to need two little helper variables, the rst is a Name object and the second one
is a parameter identi er. The name is going to be used when we display navigation links and
the parameterId will be helpful when we try to nd a model based on a path component.

Finally, we can extend the ModelController interface and provide two generic methods to
return an identi er value based on a request parameter and one more function to nd a
model by an identi er or abort with a not found error. We'll need these functions later on.

/// FILE: Sources/App/Framework/Controllers/ModelController.swift

import Vapor

154
fi
fi
fi
fi
fi
fi
fi
import Fluent

public struct Name {

let singular: String


let plural: String

init(
singular: String,
plural: String? = nil
) {
self.singular = singular
self.plural = plural ?? singular + "s"
}
}

public protocol ModelController {


associatedtype DatabaseModel: DatabaseModelInterface

var modelName: Name { get }


var parameterId: String { get }

func identifier(
_ req: Request
) throws -> UUID

func findBy(
_ id: UUID,
on: Database
) async throws -> DatabaseModel
}

extension ModelController {

func identifier(_ req: Request) throws -> UUID {


guard
let id = req.parameters.get(parameterId),
let uuid = UUID(uuidString: id)
else {
throw Abort(.badRequest)
}
return uuid
}

func findBy(
_ id: UUID,
on db: Database
) async throws -> DatabaseModel {
guard
let model = try await DatabaseModel.find(id, on: db)
else {
throw Abort(.notFound)
}
return model
}
}

Now that we have a common model controller interface, we should focus a bit on the
template for the list view. These templates will be provided by the admin module, so we're
going to place both the context and template les under the Admin/Templates directory.

The AdminListPageContext object will have a title property, a table context value, a
navigation value, and a breadcrumb array with LinkContext items. The additional navigation
links are going to be displayed under the list title and the breadcrumb is going to be used on
the top left side of the screen allowing the user to move up one or more levels in the admin
link structure if needed.

/// FILE: Sources/App/Modules/Admin/Templates/Contexts/AdminListPageContext.swift

155
fi
public struct AdminListPageContext {

public let title: String


public let table: TableContext
public let navigation: [LinkContext]
public let breadcrumbs: [LinkContext]

public init(
title: String,
table: TableContext,
navigation: [LinkContext] = [],
breadcrumbs: [LinkContext] = []
) {
self.title = title
self.table = table
self.navigation = navigation
self.breadcrumbs = breadcrumbs
}
}

In the render method of the AdminListPageTemplate we can check if the table context
contains rows and display an empty list message if it was empty, otherwise we render a
TableTemplate accordingly. We also pass the title and the array of breadcrumbs to the index
template via the context; we'll make the necessary changes right after this section. Under
the title, we render the navigation links by using the LinkTemplate struct.

/// FILE: Sources/App/Modules/Admin/Templates/Html/AdminListPageTemplate.swift

import Vapor
import SwiftHtml

public struct AdminListPageTemplate: TemplateRepresentable {

var context: AdminListPageContext

public init(
_ context: AdminListPageContext
) {
self.context = context
}

@TagBuilder
public func render(
_ req: Request
) -> Tag {
AdminIndexTemplate(
.init(
title: context.title,
breadcrumbs: context.breadcrumbs
)
) {
Div {
H1(context.title)
P {
context.navigation.map { LinkTemplate($0).render(req) }
}
}
.class("lead")

if context.table.rows.isEmpty {
Div {
Span("🔍 ")
.class("icon")
H2("Oh no")
P("This list is empty right now.")
A("Try again →")
.href(req.url.path)
.class("button-1")

156
}
.class(["lead", "container", "center"])
}
else {
TableTemplate(context.table).render(req)
}
}
.render(req)
}
}

Now it's time to add support for the breadcrumb menu in the AdminIndexTemplate. So, to
begin, we need to update AdminIndexContext to add the breadcrumbs.

/// FILE: Sources/App/Modules/Admin/Templates/Contexts/AdminIndexContext.swift

public struct AdminIndexContext {


public let title: String
public let breadcrumbs: [LinkContext]

public init(
title: String,
breadcrumbs: [LinkContext] = []
) {
self.title = title
self.breadcrumbs = breadcrumbs
}
}

And then add the code to display the bread crumbs on the page.

/// FILE: Sources/App/Modules/Admin/Templates/Html/AdminIndexTemplate.swift

import Vapor
import SwiftHtml
import SwiftSvg

public struct AdminIndexTemplate: TemplateRepresentable {

// ...

@TagBuilder
public func render(_ req: Request) -> Tag {

// ...
// .id(navigation)

Div {
Nav {
A("Admin")
.href("/admin/")

for breadcrumb in context.breadcrumbs {


LinkTemplate(breadcrumb).render(req)
}
}
}
.class("breadcrumb")

// Main
// ...
}

Since the templates are mostly ready, we can now create the generic list controller that's
going to put together all the necessary stu to render our lists. We're going to name all of
157
ff
our list-related methods with a list pre x so we won't have naming collisions. This pre xed
pattern will apply to every single admin controller that we're going to create in the future.

The rst list method is only responsible for querying all the database models. We can use
the associated DatabaseModel for this purpose, since it's a generic Fluent model the query
method is available on it.

The listView function is the request handler method, but it'd be quite complicated so we're
going to de ne several additional helpers to be able to process the request.

With the listColumns function developers will be able to de ne the set of columns that are
going to be used to render the list headers. The listCells method can be used to return the
cells for each row, it has an extra parameter so we can display the right values using the
properties of the database model.

By implementing a custom listNavigation and listBreadcrumbs you can set up custom links
for the list controller, but these methods are going to be optional since we can return with
the default links in a generic way.

The listContext method will be responsible for building the AdminListPageContext struct
and the listTemplate will return the list template. You can also override this method to use in
a custom view for your lists, but in most cases, you'll only have to de ne custom columns
and a custom cells method.

/// FILE: Sources/App/Modules/Admin/Controllers/AdminListController.swift

import Vapor

protocol AdminListController: ModelController {

func list(_ req: Request) async throws -> [DatabaseModel]


func listView(_ req: Request) async throws -> Response

func listColumns() -> [ColumnContext]


func listCells(for model: DatabaseModel) -> [CellContext]
func listNavigation(_ req: Request) -> [LinkContext]
func listBreadcrumbs(_ req: Request) -> [LinkContext]

func listContext(
_ req: Request,
_ list: [DatabaseModel]
) -> AdminListPageContext

func listTemplate(
_ req: Request,
_ list: [DatabaseModel]
) -> TemplateRepresentable
}

extension AdminListController {

func list(_ req: Request) async throws -> [DatabaseModel] {


try await DatabaseModel.query(on: req.db).all()
}

func listView(_ req: Request) async throws -> Response {


let list = try await list(req)
let template = listTemplate(req, list)
return req.templates.renderHtml(template)
}

func listNavigation(_ req: Request) -> [LinkContext] {

158
fi
fi
fi
fi
fi
fi
[
LinkContext(label: "Create",path: "create")
]
}

func listBreadcrumbs(_ req: Request) -> [LinkContext] {


[
LinkContext(
label: DatabaseModel.Module.identifier.capitalized,
dropLast: 1
)
]
}

func listContext(
_ req: Request,
_ list: [DatabaseModel]
) -> AdminListPageContext {
let rows = list.map {
RowContext(id: $0.id!.uuidString, cells: listCells(for: $0))
}
let table = TableContext(columns: listColumns(), rows: rows, actions: [
LinkContext(label: "Update", path: "update"),
LinkContext(label: "Delete", path: "delete")
])
return .init(
title: "List",
table: table,
navigation: listNavigation(req),
breadcrumbs: listBreadcrumbs(req)
)
}

func listTemplate(
_ req: Request,
_ list: [DatabaseModel]
) -> TemplateRepresentable {
AdminListPageTemplate(listContext(req, list))
}
}

Now we should update the BlogPostAdminController, we can simply replace the old nd
and listView methods with the snippet below, also don't forget to conform to the
AdminListController protocol, since that's going to provide the rest of our list view logic. You
can substitute try await nd(req) with try await ndBy(identi er(req), on: req.db) in the rest
of the methods.

Inside the listColumns we de ne the columns that we're going to display, in our case this is
going to be an image and a title column, then inside the listCells we simply return with a cell
for each column. The admin list controller will iterate through the rows and it'll call this
method for each row, giving us the ability to return the proper cells.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogPostAdminController.swift

import Vapor
import Fluent

struct BlogPostAdminController: AdminListController {


typealias DatabaseModel = BlogPostModel

let modelName: Name = .init(singular: "post")


let parameterId: String = "postId"

func listColumns() -> [ColumnContext] {


[
.init("image"),
.init("title"),

159
fi
fi
fi
fi
fi
]
}

func listCells(for model: DatabaseModel) -> [CellContext] {


[
.init(model.imageKey, type: .image),
.init(model.title, link: .init(label: model.title)),
]
}

// ...
}

Similarly, we can create a new controller for post categories. It's much better than replicating
the original listView and nd methods, since we can focus on the actual data representation
instead of the underlying query mechanism.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogCategoryAdminController.swift

import Vapor
import Fluent

struct BlogCategoryAdminController: AdminListController {


typealias DatabaseModel = BlogCategoryModel

let modelName: Name = .init(singular: "category", plural: "categories")


let parameterId: String = "categoryId"

func listColumns() -> [ColumnContext] {


[
.init("title"),
]
}

func listCells(for model: DatabaseModel) -> [CellContext] {


[
.init(model.title, link: .init(label: model.title)),
]
}
}

Register the newly created controller using the BlogRouter.

/// FILE: Sources/App/Modules/Blog/BlogRouter.swift

import Vapor

struct BlogRouter: RouteCollection {

let controller = BlogFrontendController()


let postAdminController = BlogPostAdminController()
let categoryAdminController = BlogCategoryAdminController()

func boot(routes: RoutesBuilder) throws {


routes.get("blog", use: controller.blogView)
routes.get(.anything, use: controller.postView)

let blog = routes


.grouped(AuthenticatedUser.redirectMiddleware(path: "/"))
.grouped("admin", "blog")

let categories = blog.grouped("categories")


categories.get(use: categoryAdminController.listView)

let posts = blog.grouped("posts")


posts.get(use: postAdminController.listView)
let postId = posts.grouped(":postId")
postId.get(use: postAdminController.detailView)
posts.get("create", use: postAdminController.createView)

160
fi
posts.post("create", use: postAdminController.createAction)
postId.get("update", use: postAdminController.updateView)
postId.post("update", use: postAdminController.updateAction)
postId.get("delete", use: postAdminController.deleteView)
postId.post("delete", use: postAdminController.deleteAction)
}
}

Also, the routing can be simpli ed by using generic setup methods, but we're going to talk
more about this in an upcoming chapter.

Finally, we should place a new link inside the AdminDashboardTemplate so we should be


able to navigate to the category list view.

/// FILE: Sources/App/Modules/Admin/Templates/Html/AdminDashboardTemplate.swift

import Vapor
import SwiftHtml

struct AdminDashboardTemplate: TemplateRepresentable {

var context: AdminDashboardContext

init(
_ context: AdminDashboardContext
) {
self.context = context
}

@TagBuilder
func render(_ req: Request) -> Tag {
AdminIndexTemplate(
.init(title: context.title)
) {
Div {
Section {
P(context.icon)
H1(context.title)
P(context.message)
}

Nav {
H2("Blog")
Ul {
Li {
A("Posts")
.href("/admin/blog/posts/")
}
Li {
A("Categories")
.href("/admin/blog/categories/")
}
}
}
}
.id("dashboard")
.class("container")
}
.render(req)
}
}

If you build and run the application you should see that both lists are working pretty well. Of
course, you can add more actions and customization to these lists, but the great thing about
having such a structure is that you can share the core components, and it's ridiculously easy
to build new lists for other database models.

161
fi
GENERIC DETAIL CONTROLLER
After the lists, we should come up with a better solution for displaying a more detailed view
of our database models. For this purpose, we're going to follow the same approach that
we've used for our list view. We apply the same principles for every single component in this
chapter, so if you understand these building blocks once you'll know more or less everything
about this pattern.

We start with a common detail context object, which is quite similar to our CellContext, but
this time we don't need an additional link to other pages. It's not necessary to have a key,
but rather than using label value, I prefer to keep both of them around just in case.

/// FILE: Sources/App/Framework/Templates/Contexts/DetailContext.swift

public struct DetailContext {

public enum `Type`: String {


case text
case image
}

public let key: String


public let label: String
public let value: String
public let type: `Type`

public init(
_ key: String,
_ value: String,
label: String? = nil,
type: `Type` = .text
) {
self.key = key
self.label = label ?? key.capitalized
self.value = value
self.type = type
}
}

Inside the DetailTemplate we can use de nition list elements (DT, DL) to render the context
based on the DetailContext type. We also check if the value is empty and display a non-
breaking space character for empty values. We also have to replace newlines with <br> tags.

/// FILE: Sources/App/Framework/Templates/Html/DetailTemplate.swift

import Vapor
import SwiftHtml

public struct DetailTemplate: TemplateRepresentable {

var context: DetailContext

public init(
_ context: DetailContext
) {
self.context = context
}

@TagBuilder
public func render(
_ req: Request
) -> Tag {
Dt(context.label)

162
fi
switch context.type {
case .text:
if context.value.isEmpty {
Dd("&nbsp;")
}
else {
Dd(
context.value.replacingOccurrences(
of: "\n",
with: "<br>"
)
)
}
case .image:
Dd {
Img(
src: context.value,
alt: context.label
)
}
}
}
}

Now we can move over to the admin module and we should come up with a context object
for the detail page template. Of course, we'll have a title, a set of navigation links, and
breadcrumbs, but apart from these properties, we're going to make a elds array with the
DetailContext items and there's an additional LinkContext array that we can use to display
actions and on the bottom of this page. This comes in handy if we'd like to add a delete
action (and we'll do that soon).

/// FILE: Sources/App/Modules/Admin/Templates/Contexts/AdminDetailPageContext.swift

public struct AdminDetailPageContext {

public let title: String


public let fields: [DetailContext]
public let navigation: [LinkContext]
public let breadcrumbs: [LinkContext]
public let actions: [LinkContext]

public init(
title: String,
fields: [DetailContext],
navigation: [LinkContext] = [],
breadcrumbs: [LinkContext] = [],
actions: [LinkContext] = []
) {
self.title = title
self.fields = fields
self.navigation = navigation
self.breadcrumbs = breadcrumbs
self.actions = actions
}
}

The AdminDetailPageTemplate is quite straightforward, since we already have a template


for most of the underlying context values, we can map everything and render the right
template inside the map blocks. We also propagate the title and the breadcrumbs for the
index template.

/// FILE: Sources/App/Modules/Admin/Templates/Html/AdminDetailPageTemplate.swift

import Vapor
import SwiftHtml

163
fi
struct AdminDetailPageTemplate: TemplateRepresentable {

var context: AdminDetailPageContext

init(
_ context: AdminDetailPageContext
) {
self.context = context
}

@TagBuilder
func render(
_ req: Request
) -> Tag {
AdminIndexTemplate(
.init(
title: context.title,
breadcrumbs: context.breadcrumbs
)
) {
Div {
Div {
H1(context.title)
for item in context.navigation {
LinkTemplate(item).render(req)
}
}
.class("lead")

Dl {
for item in context.fields {
DetailTemplate(item).render(req)
}
}

Section {
for item in context.actions {
LinkTemplate(item).render(req)
}
}
}
.class("container")
}
.render(req)
}
}

Let's make the generic AdminDetailController controller, we're going to follow the same
principles that we had for the list controller. This time you can de ne the displayed eld by
implementing the detailFields method. The model is going to be passed around as an
argument so developers can use the proper eld values.

/// FILE: Sources/App/Modules/Admin/Controllers/AdminDetailController.swift

import Vapor

protocol AdminDetailController: ModelController {

func detailView(
_ req: Request
) async throws -> Response

func detailTemplate(
_ req: Request,
_ model: DatabaseModel
) -> TemplateRepresentable

func detailFields(
for model: DatabaseModel
) -> [DetailContext]

164
fi
fi
fi
func detailContext(
_ req: Request,
_ model: DatabaseModel
) -> AdminDetailPageContext

func detailBreadcrumbs(
_ req: Request,
_ model: DatabaseModel
) -> [LinkContext]

func detailNavigation(
_ req: Request,
_ model: DatabaseModel
) -> [LinkContext]
}

extension AdminDetailController {

func detailView(
_ req: Request
) async throws -> Response {
let model = try await findBy(identifier(req), on: req.db)
return req.templates.renderHtml(detailTemplate(req, model))
}

func detailTemplate(
_ req: Request,
_ model: DatabaseModel
) -> TemplateRepresentable {
AdminDetailPageTemplate(detailContext(req, model))
}

func detailContext(
_ req: Request,
_ model: DatabaseModel
) -> AdminDetailPageContext {
let path = "/delete/?redirect=" +
req.url.path.pathComponents.dropLast().string +
"&cancel=" +
req.url.path

return .init(
title: "Details",
fields: detailFields(for: model),
navigation: detailNavigation(req, model),
breadcrumbs: detailBreadcrumbs(req, model),
actions: [
LinkContext(
label: "Delete",
path: path
),
]
)
}

func detailBreadcrumbs(
_ req: Request,
_ model: DatabaseModel
) -> [LinkContext] {
[
LinkContext(
label: DatabaseModel.Module.identifier.capitalized,
dropLast: 2
),
LinkContext(
label: modelName.plural.capitalized,
dropLast: 1
),
]
}

func detailNavigation(
_ req: Request,

165
_ model: DatabaseModel
) -> [LinkContext] {
[
LinkContext(
label: "Update",
path: "update"
),
]
}
}

In the detailContext we've added a new link action that we can use later on to delete the
model. The delete route will support a redirect and a cancel query parameter, this will allow
us to perform a redirection after a successful delete event or go back to the original page
that initiated the delete request.

After nishing the AdminDetailController protocol, we can update the category controller.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogCategoryAdminController.swift

import Vapor
import Fluent

struct BlogCategoryAdminController:
AdminListController,
AdminDetailController
{
typealias DatabaseModel = BlogCategoryModel

let modelName: Name = .init(


singular: "category",
plural: "categories"
)
let parameterId: String = "categoryId"

func listColumns() -> [ColumnContext] {


[
.init("title"),
]
}

func listCells(
for model: DatabaseModel
) -> [CellContext] {
[
.init(
model.title,
link: .init(label: model.title)
),
]
}

func detailFields(
for model: DatabaseModel
) -> [DetailContext] {
[
.init("title", model.title),
]
}
}

Now that we have a detail handler, we can also register the category detail route.

/// FILE: Sources/App/Modules/Blog/BlogRouter.swift

import Vapor

166
fi
struct BlogRouter: RouteCollection {

let controller = BlogFrontendController()


let postAdminController = BlogPostAdminController()
let categoryAdminController = BlogCategoryAdminController()

func boot(routes: RoutesBuilder) throws {


routes.get("blog", use: controller.blogView)
routes.get(.anything, use: controller.postView)

let blog = routes


.grouped(AuthenticatedUser.redirectMiddleware(path: "/"))
.grouped("admin", "blog")

let categories = blog.grouped("categories")


categories.get(use: categoryAdminController.listView)
let categoryId = categories.grouped(":categoryId")
categoryId.get(use: categoryAdminController.detailView)

let posts = blog.grouped("posts")


posts.get(use: postAdminController.listView)
let postId = posts.grouped(":postId")
postId.get(use: postAdminController.detailView)
posts.get("create", use: postAdminController.createView)
posts.post("create", use: postAdminController.createAction)
postId.get("update", use: postAdminController.updateView)
postId.post("update", use: postAdminController.updateAction)
postId.get("delete", use: postAdminController.deleteView)
postId.post("delete", use: postAdminController.deleteAction)
}
}

Don't forget to update the blog post controller and remove the unnecessary detailView
method.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogPostAdminController.swift

import Vapor
import Fluent

struct BlogPostAdminController:
AdminListController,
AdminDetailController
{
typealias DatabaseModel = BlogPostModel

let modelName: Name = .init(singular: "post")


let parameterId: String = "postId"

func listColumns() -> [ColumnContext] {


[
.init("image"),
.init("title"),
]
}

func listCells(for model: DatabaseModel) -> [CellContext] {


[
.init(model.imageKey, type: .image),
.init(model.title, link: .init(label: model.title)),
]
}

func detailFields(for model: DatabaseModel) -> [DetailContext] {


[
.init("image", model.imageKey, type: .image),
.init("title", model.title),
]
}

// ...

167
}

We're ready with the detail screens, and since we've already placed a link context on the
cells, we can simply navigate to the detail pages from the lists. Feel free to try out these new
pages.

GENERIC EDITOR CONTROLLERS


In the previous chapters we've learned how to build AbstractForm subclass to display
custom forms, but there are a few problems with that approach. Maybe the most intriguing
issue is the constant [unowned self] snippet in the event handler blocks.

Let's tackle this problem, but before we do that we're going to decompose the render
method just a bit by introducing a getContext function on the AbstractForm class. This will
allow us to retrieve the FormContext struct if needed.

/// FILE: Sources/App/Framework/Form/AbstractForm.swift

import Vapor

open class AbstractForm: FormComponent {

// ...

open func render(req: Request) -> TemplateRepresentable {


FormTemplate(getContext(req))
}

func getContext(_ req: Request) -> FormContext {


.init(
action: action,
fields: fields.map { $0.render(req: req)},
error: error,
submit: submit
)
}
}

So how do we solve the unowned issue? Well, we can eliminate that problem if we don't
work with classes, but with structs. Since structs aren't referenced types, we won't need an
unowned pointer inside the event handlers. For this purpose, we're going to introduce a
new ModelEditorInterface protocol that'll allow us to edit a database model using an
abstract form instance. This protocol will also conform to the FormComponent protocol, but
it's going to simply forward the event methods to the underlying form.

/// FILE: Sources/App/Framework/ModelEditorInterface.swift

import Vapor

public protocol ModelEditorInterface: FormComponent {


associatedtype Model: DatabaseModelInterface

var model: Model { get }


var form: AbstractForm { get }

init(model: Model, form: AbstractForm)

@FormComponentBuilder

168
var formFields: [FormComponent] { get }
}

public extension ModelEditorInterface {

func load(req: Request) async throws {


try await form.load(req: req)
}

func process(req: Request) async throws {


try await form.process(req: req)
}

func validate(req: Request) async throws -> Bool {


try await form.validate(req: req)
}

func write(req: Request) async throws {


try await form.write(req: req)
}

func save(req: Request) async throws {


try await form.save(req: req)
}

func read(req: Request) async throws {


try await form.read(req: req)
}

func render(req: Request) -> TemplateRepresentable {


form.render(req: req)
}
}

From now on we should always use model editors if it comes to editing database models.
Form sub-classes can still be utilized for other kinds of web forms when there's no need for
associated database entities. For example, the UserLoginForm is a great example for this
purpose.

We're going to need an editor context, it's going to be very similar to the detail context, but
this time instead of using an array of elds, we can simply use a form context.

/// FILE: Sources/App/Modules/Admin/Templates/Contexts/AdminEditorPageContext.swift

public struct AdminEditorPageContext {

public let title: String


public let form: FormContext
public let navigation: [LinkContext]
public let breadcrumbs: [LinkContext]
public let actions: [LinkContext]

public init(
title: String,
form: FormContext,
navigation: [LinkContext] = [],
breadcrumbs: [LinkContext] = [],
actions: [LinkContext] = []
) {
self.title = title
self.form = form
self.navigation = navigation
self.breadcrumbs = breadcrumbs
self.actions = actions
}
}

169
fi
The AdminEditorPageTemplate should also look familiar because it uses the same
approach, but this time we render a FormTemplate using the form context value.

We could use one more layer of abstraction with a generic context and a type alias to reduce
duplicated code, or we could also introduce a common page template that could feature a
tag builder to remove even more template-related duplication, but for now, the current
approach will just do it.

/// FILE: Sources/App/Modules/Admin/Templates/Html/AdminEditorPageTemplate.swift

import Vapor
import SwiftHtml

struct AdminEditorPageTemplate: TemplateRepresentable {

var context: AdminEditorPageContext

init(
_ context: AdminEditorPageContext
) {
self.context = context
}

@TagBuilder
func render(
_ req: Request
) -> Tag {
AdminIndexTemplate(
.init(
title: context.title,
breadcrumbs: context.breadcrumbs
)
) {
Div {
Div {
H1(context.title)
for item in context.navigation {
LinkTemplate(item).render(req)
}
}
.class("lead")

FormTemplate(context.form).render(req)

Section {
for item in context.actions {
LinkTemplate(item).render(req)
}
}
}
.class("container")
}
.render(req)
}
}

We also need a generic create controller, we can take advantage of the CreateModelEditor
protocol and use that to initialize and process the input form and update the underlying
model based on the submitted form values. Once again we pre x everything to avoid
naming collisions.

/// FILE: Sources/App/Modules/Admin/Controllers/AdminCreateController.swift

import Vapor

protocol AdminCreateController: ModelController {

170
fi
associatedtype CreateModelEditor: ModelEditorInterface

func createTemplate(
_ req: Request,
_ editor: CreateModelEditor
) -> TemplateRepresentable

func createView(
_ req: Request
) async throws -> Response

func createAction(
_ req: Request
) async throws -> Response

func createContext(
_ req: Request,
_ editor: CreateModelEditor
) -> AdminEditorPageContext

func createBreadcrumbs(
_ req: Request
) -> [LinkContext]
}

extension AdminCreateController {

private func render(


_ req: Request,
editor: CreateModelEditor
) -> Response {
req.templates.renderHtml(
createTemplate(req, editor)
)
}

func createView(
_ req: Request
) async throws -> Response {
let editor = CreateModelEditor(
model: .init(),
form: .init()
)
editor.form.fields = editor.formFields
try await editor.load(req: req)
return render(req, editor: editor)
}

func createAction(
_ req: Request
) async throws -> Response {
let model = DatabaseModel()
let editor = CreateModelEditor(
model: model as! CreateModelEditor.Model,
form: .init()
)
editor.form.fields = editor.formFields
try await editor.load(req: req)
try await editor.process(req: req)
let isValid = try await editor.validate(req: req)
guard isValid else {
return render(req, editor: editor)
}
try await editor.write(req: req)
try await editor.model.create(on: req.db)
try await editor.save(req: req)
var components = req.url.path.pathComponents.dropLast()
components += editor.model.id!.uuidString.pathComponents
return req.redirect(to: "/" + components.string + "/update/")
}

func createTemplate(
_ req: Request,
_ editor: CreateModelEditor

171
) -> TemplateRepresentable {
AdminEditorPageTemplate(
createContext(req, editor)
)
}

func createContext(
_ req: Request,
_ editor: CreateModelEditor
) -> AdminEditorPageContext {
let context = FormContext(
action: editor.form.action,
fields: editor.form.fields.map { $0.render(req: req) },
error: editor.form.error,
submit: editor.form.submit
)
return .init(
title: "Create",
form: context,
breadcrumbs: createBreadcrumbs(req)
)
}

func createBreadcrumbs(
_ req: Request
) -> [LinkContext] {
[
LinkContext(
label: DatabaseModel.Module.identifier.capitalized,
dropLast: 2
),
LinkContext(
label: modelName.plural.capitalized,
dropLast: 1
),
]
}
}

Just like the create controller, we can introduce an update controller which will contain all
the update-related logic. As you can see after the create and update action, we redirect to a
new URL, but for now, we can suppose that an update/detail route will be available. Later on,
you might want to decompose the redirection logic by introducing a new redirect function
that can be customized inside the actual controller implementation.

/// FILE: Sources/App/Modules/Admin/Controllers/AdminUpdateController.swift

import Vapor

protocol AdminUpdateController: ModelController {


associatedtype UpdateModelEditor: ModelEditorInterface

func updateView(
_ req: Request
) async throws -> Response

func updateAction(
_ req: Request
) async throws -> Response

func updateTemplate(
_ req: Request,
_ editor: UpdateModelEditor
) async -> TemplateRepresentable

func updateContext(
_ req: Request,
_ editor: UpdateModelEditor
) async -> AdminEditorPageContext

172
func updateBreadcrumbs(
_ req: Request,
_ model: DatabaseModel
) -> [LinkContext]

func updateNavigation(
_ req: Request,
_ model: DatabaseModel
) -> [LinkContext]
}

extension AdminUpdateController {

private func render(


_ req: Request,
editor: UpdateModelEditor
) async -> Response {
req.templates.renderHtml(
await updateTemplate(req, editor)
)
}

func updateView(
_ req: Request
) async throws -> Response {
let model = try await findBy(identifier(req), on: req.db)
let editor = UpdateModelEditor(
model: model as! UpdateModelEditor.Model,
form: .init()
)
editor.form.fields = editor.formFields
try await editor.load(req: req)
try await editor.read(req: req)
return await render(req, editor: editor)
}

func updateAction(
_ req: Request
) async throws -> Response {
let model = try await findBy(identifier(req), on: req.db)
let editor = UpdateModelEditor(
model: model as! UpdateModelEditor.Model,
form: .init()
)
editor.form.fields = editor.formFields
try await editor.load(req: req)
try await editor.process(req: req)
let isValid = try await editor.validate(req: req)
guard isValid else {
return await render(req, editor: editor)
}
try await editor.write(req: req)
try await editor.model.update(on: req.db)
try await editor.save(req: req)
return req.redirect(to: req.url.path)
}

func updateTemplate(
_ req: Request,
_ editor: UpdateModelEditor
) async -> TemplateRepresentable {
await AdminEditorPageTemplate(
updateContext(req, editor)
)
}

func updateContext(
_ req: Request,
_ editor: UpdateModelEditor
) async -> AdminEditorPageContext {
let path = "delete/?redirect=" +
req.url.path.pathComponents.dropLast(2).string +
"&cancel=" +
req.url.path

173
let model = editor.model as! DatabaseModel
return .init(
title: "Update",
form: editor.form.getContext(req),
navigation: updateNavigation(req, model),
breadcrumbs: updateBreadcrumbs(req, model),
actions: [
LinkContext(
label: "Delete",
path: path,
dropLast: 1
),
]
)
}

func updateBreadcrumbs(
_ req: Request,
_ model: DatabaseModel
) -> [LinkContext] {
[
LinkContext(
label: DatabaseModel.Module.identifier.capitalized,
dropLast: 3
),
LinkContext(
label: modelName.plural.capitalized,
dropLast: 2
),
]
}

func updateNavigation(
_ req: Request,
_ model: DatabaseModel
) -> [LinkContext] {
[
LinkContext(
label: "Details",
dropLast: 1
),
]
}
}

The create and update controller will take care of the form rendering and submission
work ow, but it won't say anything about how the form elds should look. For this purpose,
we have to create form editors.

The main goal of an editor is to provide the necessary form elds required to display a
create or edit form that's going to be rendered using the controllers on the admin interface.

Fortunately, we already have quite a lot of form eld types available so we can use those
components and a builder method to create our elds.

/// FILE: Sources/App/Modules/Blog/Editors/BlogCategoryEditor.swift

import Vapor

struct BlogCategoryEditor: ModelEditorInterface {

let model: BlogCategoryModel


let form: AbstractForm

init(
model: BlogCategoryModel,
form: AbstractForm
) {

174
fl
fi
fi
fi
fi
self.model = model
self.form = form
}

@FormComponentBuilder
var formFields: [FormComponent] {
InputField("title")
.config {
$0.output.context.label.required = true
}
.validators {
FormFieldValidator.required($1)
}
.read { $1.output.context.value = model.title }
.write { model.title = $1.input }
}
}

For the BlogCategoryModel we only have to edit a title value; for this purpose an InputField
is the perfect candidate. Validation is also ridiculously easy and the only thing we have to do
here is to read and write the input and output values when the controller calls the
appropriate editor method.

Inside the BlogCategoryAdminController we just have to implement the create and update
protocols; this happens through two new type alias de nitions. This is how we tell the
system the exact type of editor that it should use.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogCategoryAdminController.swift

import Vapor
import Fluent

struct BlogCategoryAdminController:
AdminListController,
AdminDetailController,
AdminCreateController,
AdminUpdateController
{
typealias DatabaseModel = BlogCategoryModel
typealias CreateModelEditor = BlogCategoryEditor
typealias UpdateModelEditor = BlogCategoryEditor

let modelName: Name = .init(


singular: "category",
plural: "categories"
)
let parameterId: String = "categoryId"

func listColumns() -> [ColumnContext] {


[
.init("title"),
]
}

func listCells(
for model: DatabaseModel
) -> [CellContext] {
[
.init(
model.title,
link: .init(label: model.title)
),
]
}

func detailFields(
for model: DatabaseModel
) -> [DetailContext] {

175
fi
[
.init("title", model.title),
]
}
}

If you take a look at the category controller we can say that it's quite simple, yet we
implemented a lot of logic through default protocol extensions.

Update the blog router and register the edit routes just like we did for the post routes.

/// FILE: Sources/App/Modules/Blog/BlogRouter.swift

import Vapor

struct BlogRouter: RouteCollection {

let controller = BlogFrontendController()


let postAdminController = BlogPostAdminController()
let categoryAdminController = BlogCategoryAdminController()

func boot(routes: RoutesBuilder) throws {


routes.get("blog", use: controller.blogView)
routes.get(.anything, use: controller.postView)

let blog = routes


.grouped(AuthenticatedUser.redirectMiddleware(path: "/"))
.grouped("admin", "blog")

let categories = blog.grouped("categories")


categories.get(use: categoryAdminController.listView)
categories.get("create", use: categoryAdminController.createView)
categories.post("create", use: categoryAdminController.createAction)
let categoryId = categories.grouped(":categoryId")
categoryId.get(use: categoryAdminController.detailView)
categoryId.get("update", use: categoryAdminController.updateView)
categoryId.post("update", use: categoryAdminController.updateAction)

let posts = blog.grouped("posts")


posts.get(use: postAdminController.listView)
let postId = posts.grouped(":postId")
postId.get(use: postAdminController.detailView)
posts.get("create", use: postAdminController.createView)
posts.post("create", use: postAdminController.createAction)
postId.get("update", use: postAdminController.updateView)
postId.post("update", use: postAdminController.updateAction)
postId.get("delete", use: postAdminController.deleteView)
postId.post("delete", use: postAdminController.deleteAction)
}
}

Now we should be able to list, view, create, and edit blog categories.

Our nal task in this chapter is to update the blog post controller; to make this happen, rst,
we should remove the original blog post form, and second, we should add an editor instead.

/// FILE: Sources/App/Modules/Blog/Editors/BlogPostEditor.swift

import Vapor

struct BlogPostEditor: ModelEditorInterface {

let model: BlogPostModel


let form: AbstractForm

var dateFormatter: DateFormatter = {


let formatter = DateFormatter()

176
fi
fi
formatter.dateStyle = .long
formatter.timeStyle = .short
return formatter
}()

init(model: BlogPostModel, form: AbstractForm) {


self.model = model
self.form = form
}

@FormComponentBuilder
var formFields: [FormComponent] {
ImageField("image", path: "blog/post")
.read {
$1.output.context.previewUrl = model.imageKey
($1 as! ImageField).imageKey = model.imageKey
}
.write {
model.imageKey = ($1 as! ImageField).imageKey ?? ""
}

InputField("slug")
.config {
$0.output.context.label.required = true
}
.validators {
FormFieldValidator.required($1)
}
.read {
$1.output.context.value = model.slug
}
.write {
model.slug = $1.input
}

InputField("title")
.config {
$0.output.context.label.required = true
}
.validators {
FormFieldValidator.required($1)
}
.read {
$1.output.context.value = model.title
}
.write {
model.title = $1.input
}

InputField("date")
.config {
$0.output.context.label.required = true
$0.output.context.value = dateFormatter.string(from: Date())
}
.validators {
FormFieldValidator.required($1)
}
.read {
$1.output.context.value = dateFormatter.string(from: model.date)
}
.write {
model.date = dateFormatter.date(from: $1.input) ?? Date()
}

TextareaField("excerpt")
.read { $1.output.context.value = model.excerpt }
.write { model.excerpt = $1.input }

TextareaField("content")
.read { $1.output.context.value = model.content }
.write { model.content = $1.input }

SelectField("category")
.load { req, field in

177
let categories = try await BlogCategoryModel
.query(on: req.db)
.all()
field.output.context.options = categories.map {
OptionContext(key: $0.id!.uuidString, label: $0.title)
}
}
.read { req, field in
field.output.context.value = model.$category.id.uuidString
}
.write { req, field in
if
let uuid = UUID(uuidString: field.input),
let category = try await BlogCategoryModel
.find(uuid, on: req.db)
{
model.$category.id = category.id!
}
}
}
}

Alter the BlogPostAdminController and remove the create and update methods from it, you
can also update the nd calls inside the delete endpoints.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogPostAdminController.swift

import Vapor
import Fluent

struct BlogPostAdminController:
AdminListController,
AdminDetailController,
AdminCreateController,
AdminUpdateController
{
typealias DatabaseModel = BlogPostModel
typealias CreateModelEditor = BlogPostEditor
typealias UpdateModelEditor = BlogPostEditor

let modelName: Name = .init(singular: "post")


let parameterId: String = "postId"

func listColumns() -> [ColumnContext] {


[
.init("image"),
.init("title"),
]
}

func listCells(for model: DatabaseModel) -> [CellContext] {


[
.init(model.imageKey, type: .image),
.init(model.title, link: .init(label: model.title)),
]
}

func detailFields(for model: DatabaseModel) -> [DetailContext] {


[
.init("image", model.imageKey, type: .image),
.init("title", model.title),
]
}

func deleteView(_ req: Request) async throws -> Response {


let model = try await findBy(identifier(req), on: req.db)

let template = BlogPostAdminDeleteTemplate(


.init(
title: "Delete post",
name: model.title,

178
fi
type: "post"
)
)
return req.templates.renderHtml(template)
}

func deleteAction(_ req: Request) async throws -> Response {


let model = try await findBy(identifier(req), on: req.db)
try await req.fs.delete(key: model.imageKey)
try await model.delete(on: req.db)
return req.redirect(to: "/admin/blog/posts/")
}
}

The very last thing that remains in the blog post controller is the delete action, but we're also
going to get rid of those functions in the next section.

GENERIC DELETE CONTROLLER


We can start again with a new page context object. This time we're going to introduce a
name and a type property. The name will hold a contextual value for an entity and the type
will be used to display the actual type of the object that the user wants to delete.

We're going to display a con rmation form, for this purpose we're going to de ne a simple
delete form, and when the user submits that form using a post method, we'll perform the
delete operation. This is why we have a FormContext inside the delete page context.

/// FILE: Sources/App/Modules/Admin/Templates/Contexts/AdminDeletePageContext.swift

public struct AdminDeletePageContext {

public let title: String


public let name: String
public let type: String
public let form: FormContext
public let navigation: [LinkContext]
public let breadcrumbs: [LinkContext]

public init(
title: String,
name: String,
type: String,
form: FormContext,
navigation: [LinkContext] = [],
breadcrumbs: [LinkContext] = []
) {
self.title = title
self.name = name
self.type = type
self.form = form
self.navigation = navigation
self.breadcrumbs = breadcrumbs
}
}

Hopefully, you'll understand the reason a little bit better for the name and type values based
on the template. For example, if you'd like to delete a 'post' type with the name of 'Lorem
Ipsum', the following message will be displayed: "You are about to permanently delete the
`Lorem Ipsum` post.".

179
fi
fi
/// FILE: Sources/App/Modules/Admin/Templates/Html/AdminDeletePageTemplate.swift

import Vapor
import SwiftHtml

public struct AdminDeletePageTemplate: TemplateRepresentable {

var context: AdminDeletePageContext

public init(
_ context: AdminDeletePageContext
) {
self.context = context
}

@TagBuilder
public func render(
_ req: Request
) -> Tag {
AdminIndexTemplate(
.init(
title: context.title,
breadcrumbs: context.breadcrumbs
)
) {
Div {
Span("🗑 ")
.class("icon")
H1(context.title)
P("You are about to permanently delete the<br>`\(context.name)` \
(context.type).")

FormTemplate(context.form).render(req)

A("Cancel")
.href((try? req.query.get(String.self, at: "cancel")) ?? "#")
.class(["button", "cancel"])
}
.class(["lead", "container", "center"])
}
.render(req)
}
}

We can check if there's a cancel query parameter and use it as a link to go back to the
original page if the user clicks the cancel button. Inside the form controller, we're going to do
the same with the redirect query parameter and use it to go to the nal location after the
deletion.

The AdminDeleteController controller is relatively simple compared to the previously


introduced admin controllers. The only required method is going to be the deleteInfo where
developers can provide a contextual value as the name of the entity that's going to be
removed.

/// FILE: Sources/App/Modules/Admin/Controllers/AdminDeleteController.swift

import Vapor

final class DeleteForm: AbstractForm {

init() {
super.init()

self.action.method = .post
self.submit = "Delete"
}
}

180
fi
protocol AdminDeleteController: ModelController {

func deleteView(
_ req: Request
) async throws -> Response

func deleteAction(
_ req: Request
) async throws -> Response

func deleteTemplate(
_ req: Request,
_ model: DatabaseModel,
_ form: DeleteForm
) -> TemplateRepresentable

func deleteInfo(
_ model: DatabaseModel
) -> String

func deleteContext(
_ req: Request,
_ model: DatabaseModel,
_ form: DeleteForm
) -> AdminDeletePageContext
}

extension AdminDeleteController {

func deleteView(
_ req: Request
) async throws -> Response {
let model = try await findBy(identifier(req), on: req.db)
let form = DeleteForm()
return req.templates.renderHtml(
deleteTemplate(req, model, form)
)
}

func deleteAction(
_ req: Request
) async throws -> Response {
let model = try await findBy(identifier(req), on: req.db)
try await model.delete(on: req.db)

var url = req.url.path


if let redirect = try? req.query.get(String.self, at: "redirect") {
url = redirect
}
return req.redirect(to: url)
}

func deleteTemplate(
_ req: Request,
_ model: DatabaseModel,
_ form: DeleteForm
) -> TemplateRepresentable {
AdminDeletePageTemplate(
deleteContext(req, model, form)
)
}

func deleteContext(
_ req: Request,
_ model: DatabaseModel,
_ form: DeleteForm
) -> AdminDeletePageContext {
.init(
title: "Delete",
name: deleteInfo(model),
type: "model",
form: form.getContext(req)
)
}

181
}

First, we can try this out by updating the BlogCategoryAdminController le.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogCategoryAdminController.swift

import Vapor
import Fluent

struct BlogCategoryAdminController:
AdminListController,
AdminDetailController,
AdminCreateController,
AdminUpdateController,
AdminDeleteController
{
typealias DatabaseModel = BlogCategoryModel
typealias CreateModelEditor = BlogCategoryEditor
typealias UpdateModelEditor = BlogCategoryEditor

let modelName: Name = .init(


singular: "category",
plural: "categories"
)
let parameterId: String = "categoryId"

func listColumns() -> [ColumnContext] {


[
.init("title"),
]
}

func listCells(
for model: DatabaseModel
) -> [CellContext] {
[
.init(
model.title,
link: .init(label: model.title)
),
]
}

func detailFields(
for model: DatabaseModel
) -> [DetailContext] {
[
.init("title", model.title),
]
}

func deleteInfo(
_ model: DatabaseModel
) -> String {
model.title
}
}

Before we register the delete routes, let's just change the post controller real quick.
/// FILE: Sources/App/Modules/Blog/Controllers/BlogPostAdminController.swift

import Vapor
import Fluent

struct BlogPostAdminController:
AdminListController,
AdminDetailController,
AdminCreateController,
AdminUpdateController,
AdminDeleteController

182
fi
{
typealias DatabaseModel = BlogPostModel
typealias CreateModelEditor = BlogPostEditor
typealias UpdateModelEditor = BlogPostEditor

let modelName: Name = .init(singular: "post")


let parameterId: String = "postId"

func listColumns() -> [ColumnContext] {


[
.init("image"),
.init("title"),
]
}

func listCells(for model: DatabaseModel) -> [CellContext] {


[
.init(model.imageKey, type: .image),
.init(model.title, link: .init(label: model.title)),
]
}

func detailFields(for model: DatabaseModel) -> [DetailContext] {


[
.init("image", model.imageKey, type: .image),
.init("title", model.title),
]
}

func deleteInfo(
_ model: DatabaseModel
) -> String {
model.title
}
}

This is how the nal router object should look for the blog module. You can de ne a helper
method to remove duplicated code, but in a future chapter, I'll show you a better method.

/// FILE: Sources/App/Modules/Blog/BlogRouter.swift

import Vapor

struct BlogRouter: RouteCollection {

let controller = BlogFrontendController()


let postAdminController = BlogPostAdminController()
let categoryAdminController = BlogCategoryAdminController()

func boot(routes: RoutesBuilder) throws {


routes.get("blog", use: controller.blogView)
routes.get(.anything, use: controller.postView)

let blog = routes


.grouped(AuthenticatedUser.redirectMiddleware(path: "/"))
.grouped("admin", "blog")

let categories = blog.grouped("categories")


categories.get(use: categoryAdminController.listView)
categories.get("create", use: categoryAdminController.createView)
categories.post("create", use: categoryAdminController.createAction)
let categoryId = categories.grouped(":categoryId")
categoryId.get(use: categoryAdminController.detailView)
categoryId.get("update", use: categoryAdminController.updateView)
categoryId.post("update", use: categoryAdminController.updateAction)
categoryId.get("delete", use: categoryAdminController.deleteView)
categoryId.post("delete", use: categoryAdminController.deleteAction)

let posts = blog.grouped("posts")


posts.get(use: postAdminController.listView)
let postId = posts.grouped(":postId")

183
fi
fi
postId.get(use: postAdminController.detailView)
posts.get("create", use: postAdminController.createView)
posts.post("create", use: postAdminController.createAction)
postId.get("update", use: postAdminController.updateView)
postId.post("update", use: postAdminController.updateAction)
postId.get("delete", use: postAdminController.deleteView)
postId.post("delete", use: postAdminController.deleteAction)
}
}

One more little thing that you can add to the admin.js le to enhance user experience is a
key down event listener. You can check if the user pressed the CMD / CTRL + S combination
and submit a form if this event happens. I really like this, since I don't have to scroll to nd
the submit or save button, but others nd it annoying, so it's totally up to your preference.

/* FILE: Public/javascript/admin.js */

document.addEventListener("keydown", function(e) {
if ( (window.navigator.platform.match("Mac") ? e.metaKey : e.ctrlKey) && e.keyCode ==
83 ) {
e.preventDefault();
document.forms[0].submit();
}
}, false);

I believe that this protocol-oriented way of describing controllers is truly magical. Anything
can be overridden, and still, everything just works thanks to the implementation of the
protocol extensions. Static dispatch is crazy fast, we're barely using classes in our entire
project, except for the abstract forms, elds, and the models, but that's ne since we have to
work with references in those cases.

SUMMARY
In this chapter, we've mostly nished the CMS for the blog module. We've created a generic
CRUD functionality that can be used by others. We've implemented everything by taking
advantage of one of the most powerful features of the Swift language. Protocol extensions
helped us to eliminate lots of boilerplate code, thanks to them we were able to clean up the
blog controllers. The next chapter will follow this path, we'll create a generic API layer for the
blog.

184
fi
fi
fi
fi
fi
fi
CHAPTER 11:
A BASIC A REST API LAYER

You'll learn about building a standard JSON-based API service. In the rst section, we'll
discuss how to design a REST API, then we'll build the CRUD endpoints for the category
controller. We'll talk a bit about the HTTP layer, and learn how to use the cURL command-line
utility to test the endpoints. You'll discover why it's a better practice to use standalone data
transfer objects instead of exposing database models to the public.

IT'S ALL ABOUT DATA MODELS AND CRUD


We've already talked about what CRUD is, in this chapter we're going to extend this
abbreviation with one more letter, so we're going to implement a CRUPD API, but nobody
refers to it like this.

In the previous chapters, we've only used GET and POST HTTP methods, because browsers
are quite special animals, they can only send these kinds of request methods without the
help of JavaScript. That's the reason we implemented all of the admin endpoints for using
only these methods.

A REST API on the other hand should use the appropriate verb for each endpoint.

- GET /api/module/model/ - list

- GET /api/module/model/:modelId/ - details

- POST /api/module/model/ - create

- PUT /api/module/model/:modelId/ - update (update every eld)

- PATCH /api/module/model/:modelId/ - patch (update some elds)

- DELETE /api/module/model/:modelId/ - delete

We're going to implement all of these endpoints for the blog category model as a starting
point. Later on, in the next chapter, we're going to turn this solution into a generic approach.

Vapor can transform models into speci c content objects and you may have seen code
implement the Content protocol directly on the database models, but I believe that this isn't
a good practice.

If we take the UserAccountModel and extend it with the Content type, we might
accidentally expose our password eld when someone asks for the list of users. We want to
avoid this scenario, so that's one of the main reasons why I always de ne standalone Data
185
fi
fi
fi
fi
fi
fi
Transfer Objects (DTOs). This requires a bit more manual e ort, but in the long run, it's going
to be worth it because Swift clients can share these DTOs and you can even build a client-
side SDK with the help of them.

The main disadvantage of using DTOs is that you have to manually map them into database
models and vice versa. If you don't like to type that much, you can always come up with a
code generator tool that'll save you quite a lot of time.

LIST API ENDPOINT


As a beginning, we should create a new BlogCategoryApiController le inside the blog
module. We're going to create two new methods inside of it. The rst one is going to allow
us to map database models into data transfer objects. The second one is going to be our
actual route handler, and we're going to su x API handlers with the Api su x.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogCategoryApiController.swift

import Vapor

extension Blog.Category.List: Content {}

struct BlogCategoryApiController {

func listOutput(
_ req: Request,
_ models: [BlogCategoryModel]
) async throws -> [Blog.Category.List] {
models.map {
.init(id: $0.id!, title: $0.title)
}
}

func listApi(
_ req: Request
) async throws -> [Blog.Category.List] {
let models = try await BlogCategoryModel
.query(on: req.db)
.all()
return try await listOutput(req, models)
}
}

Since we already created a DTO for the categories, we just have to extend the
Blog.Category.List struct to conform to the Content protocol, this will allow us to return an
array of items and Vapor can take care of encoding them as a response object.

Inside the BlogRouter we should create an instance of this new API controller and register
the listRoute on the categories endpoint as a get request handler.

/// FILE: Sources/App/Modules/Blog/BlogRouter.swift

import Vapor

struct BlogRouter: RouteCollection {

let controller = BlogFrontendController()


let postAdminController = BlogPostAdminController()
let categoryAdminController = BlogCategoryAdminController()

186
ffi
ff
fi
fi
ffi
let categoryApiController = BlogCategoryApiController()

func boot(routes: RoutesBuilder) throws {

// ...

let blogApi = routes.grouped("api", "blog")


let categoriesApi = blogApi.grouped("categories")
categoriesApi.get(use: categoryApiController.listApi)
}
}

That's it, now it's possible to retrieve categories through an API call. You can try it out by
going to the /api/blog/categories/ endpoint using your browser, but let me introduce you to
a new command-line tool called cURL.

Note: Make sure you always use a trailing slash.

It's possible to make various HTTP requests using the curl command. Without additional
parameters, it'll make a GET request by default, so this is how you can get the category list.

curl "http://127.0.0.1:8080/api/blog/categories/"

The command above should return the list of the available categories in JSON format. The
output of the result isn't so nice, but fortunately, if you're on macOS you can reformat the
JSON response with the json_pp tool or via a little python snippet.

curl "http://127.0.0.1:8080/api/blog/categories/"|json_pp
curl "http://127.0.0.1:8080/api/blog/categories/"|python -m json.tool

If you install the pygments package or the open-source jq tool you can enable proper syntax
highlight for the JSON response. It's also possible to make an alias, I prefer to simply use
json to reformat my cURL outputs.

brew install pygments


brew install jq

curl "http://127.0.0.1:8080/api/blog/categories/"|json_pp|pygmentize -l json


curl "http://127.0.0.1:8080/api/blog/categories/"|python -m json.tool|pygmentize -l json
curl "http://127.0.0.1:8080/api/blog/categories/"|jq

alias json='python -m json.tool'


alias json='json_pp|pygmentize -l json'
alias json='jq'

curl "http://127.0.0.1:8080/api/blog/categories/"|json

In a nutshell that's how cURL works, but as we move forward I'll show you a few more
options and command ags as well.

187
fl
DETAIL API ENDPOINT
The next endpoint that we're going to build is the details query. For this purpose, we have to
create a new Detail data transfer object inside the Blog.Category extension.

/// FILE: Sources/App/Modules/Blog/Objects/BlogCategory.swift

import Foundation

extension Blog.Category {

struct List: Codable {


let id: UUID
let title: String
}

struct Detail: Codable {


let id: UUID
let title: String
}
}

To make things even more simple, we can conform the BlogCategoryApiController to the
ModelController protocol, this way we can reuse the ndBy method inside our detailApi
function. For now, you don't have to replace BlogCategoryModel references with the
DatabaseModel type alias, we're going to generalize everything in the next chapter.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogCategoryApiController.swift

import Vapor

extension Blog.Category.List: Content {}


extension Blog.Category.Detail: Content {}

struct BlogCategoryApiController: ModelController {


typealias DatabaseModel = BlogCategoryModel

var modelName: Name = .init(


singular: "category",
plural: "categories"
)
var parameterId: String = "categoryId"

func listOutput(
_ req: Request,
_ models: [BlogCategoryModel]
) async throws -> [Blog.Category.List] {
models.map {
.init(id: $0.id!, title: $0.title)
}
}

func listApi(
_ req: Request
) async throws -> [Blog.Category.List] {
let models = try await BlogCategoryModel
.query(on: req.db)
.all()
return try await listOutput(req, models)
}

func detailOutput(
_ req: Request,
_ model: BlogCategoryModel
) async throws -> Blog.Category.Detail {
.init(

188
fi
id: model.id!,
title: model.title
)
}

func detailApi(
_ req: Request
) async throws -> Blog.Category.Detail {
let model = try await findBy(identifier(req), on: req.db)
return try await detailOutput(req, model)
}
}

In the router we should register this new detail API handler on the categoryApiId route.

/// FILE: Sources/App/Modules/Blog/BlogRouter.swift

import Vapor

struct BlogRouter: RouteCollection {

let controller = BlogFrontendController()


let postAdminController = BlogPostAdminController()
let categoryAdminController = BlogCategoryAdminController()
let categoryApiController = BlogCategoryApiController()

func boot(routes: RoutesBuilder) throws {

// ...

let blogApi = routes.grouped("api", "blog")


let categoriesApi = blogApi.grouped("categories")
categoriesApi.get(use: categoryApiController.listApi)
let categoryApiId = categoriesApi.grouped(":categoryId")
categoryApiId.get(use: categoryApiController.detailApi)
}
}

If you're curious about the underlying HTTP response you can append the -i ag to curl, but
in this case, you won't be able to prettify the JSON output, because of the extra info.

curl -i "http://127.0.0.1:8080/api/blog/categories/[uuid]/"

If you enter a valid unique identi er you should be able to get a detailed category object
with a success (200) response, otherwise, you'll get a bad request (400 - if the UUID was
invalid) or not found (404 - if the UUID was valid, but there's no entity) HTTP status code.

CREATE API ENDPOINT


The create API endpoint is going to be a bit di erent, since this time we're going to need
some sort of input value that we can use to create our database model. For this purpose
we'll de ne a new Create object inside the blog category namespace.

/// FILE: Sources/App/Modules/Blog/Objects/BlogCategory.swift

import Foundation

extension Blog.Category {

struct List: Codable {

189
fi
fi
ff
fl
let id: UUID
let title: String
}

struct Detail: Codable {


let id: UUID
let title: String
}

struct Create: Codable {


let title: String
}
}

Now in the API controller, we'll need two new methods, the rst one is going to map the
input to the database model. For the categories, this means that we simply set the model
title based on the input title. We don't validate the input here, because in Chapter 12 we're
going to introduce a better way to handle input validation.

The second method is the actual API handler, we can use the req.content.decode method to
initialize the input type, then we can create a new database model and call the mapper on it.
After we've successfully mapped the input, we can persist the model into the database by
calling the create method and nally, we can respond with a detailOutput response.

Calling the detailOutput isn't the best practice, because it'll create a dependency between
the create and detail endpoints, but in the next chapter, I'll show you how to get rid of this.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogCategoryApiController.swift

import Vapor

extension Blog.Category.List: Content {}


extension Blog.Category.Detail: Content {}

struct BlogCategoryApiController: ModelController {

// ...

func createInput(
_ req: Request,
_ model: BlogCategoryModel,
_ input: Blog.Category.Create
) async throws {
model.title = input.title
}

func createApi(
_ req: Request
) async throws -> Response {
let input = try req.content.decode(Blog.Category.Create.self)
let model = DatabaseModel()
try await createInput(req, model, input)
try await model.create(on: req.db)
return try await detailOutput(req, model)
.encodeResponse(status: .created, for: req)
}
}

We can simply register the createApi handler as a POST request handler in the router.

/// FILE: Sources/App/Modules/Blog/BlogRouter.swift

import Vapor

struct BlogRouter: RouteCollection {

190
fi
fi
let controller = BlogFrontendController()
let postAdminController = BlogPostAdminController()
let categoryAdminController = BlogCategoryAdminController()
let categoryApiController = BlogCategoryApiController()

func boot(routes: RoutesBuilder) throws {

// ...

let blogApi = routes.grouped("api", "blog")


let categoriesApi = blogApi.grouped("categories")
categoriesApi.get(use: categoryApiController.listApi)
let categoryApiId = categoriesApi.grouped(":categoryId")
categoryApiId.get(use: categoryApiController.detailApi)
categoriesApi.post(use: categoryApiController.createApi)
}
}

Run the server and we can use cURL to test this endpoint. With the -X ag, you can specify
the HTTP method; in our case, it's going to be POST and we also have to set a custom
header eld called Content-Type with an application/json value. This happens through the
-H ag.

The header is required because we're going to send a JSON object inside the HTTP POST
body. This can be done via the -d ag.

Please note that these cURL ags are case-sensitive, so make sure that everything is written
as it's in the example.

curl -i \
-X POST "http://127.0.0.1:8080/api/blog/categories" \
-H "Content-Type: application/json" \
-d '{"title": "Lorem ipsum"}'

The command should return the newly created object with the complete detail structure.

UPDATE API ENDPOINT


The update command will be very similar to the create, so again we start with a DTO.

/// FILE: Sources/App/Modules/Blog/Objects/BlogCategory.swift

import Foundation

extension Blog.Category {

// ...

struct Update: Codable {


let title: String
}
}

Inside the controller, we create two more new methods with the update pre x, and this time
in the update handler we try to look for an existing model based on the identi er. If we found

191
fl
fi
fl
fl
fl
fi
fi
the object we can safely map the new values by calling the updateInput function, then we
can respond again with a detail object.

Please note that in content creation there was a special created (201) HTTP status code, but
when you update something you can simply respond with a regular OK (200) code.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogCategoryApiController.swift

import Vapor

extension Blog.Category.List: Content {}


extension Blog.Category.Detail: Content {}

struct BlogCategoryApiController: ModelController {

// ...

func updateInput(
_ req: Request,
_ model: BlogCategoryModel,
_ input: Blog.Category.Update
) async throws {
model.title = input.title
}

func updateApi(
_ req: Request
) async throws -> Response {
let model = try await findBy(identifier(req), on: req.db)
let input = try req.content.decode(Blog.Category.Update.self)
try await updateInput(req, model, input)
try await model.update(on: req.db)
return try await detailOutput(req, model)
.encodeResponse(for: req)
}
}

The update term usually involves a PUT HTTP request, so we register this handler
accordingly.

/// FILE: Sources/App/Modules/Blog/BlogRouter.swift

import Vapor

struct BlogRouter: RouteCollection {

let controller = BlogFrontendController()


let postAdminController = BlogPostAdminController()
let categoryAdminController = BlogCategoryAdminController()
let categoryApiController = BlogCategoryApiController()

func boot(routes: RoutesBuilder) throws {

// ...

let blogApi = routes.grouped("api", "blog")


let categoriesApi = blogApi.grouped("categories")
categoriesApi.get(use: categoryApiController.listApi)
let categoryApiId = categoriesApi.grouped(":categoryId")
categoryApiId.get(use: categoryApiController.detailApi)
categoriesApi.post(use: categoryApiController.createApi)
categoryApiId.put(use: categoryApiController.updateApi)
}
}

192
We can use the following cURL snippet to update an existing model. You should use a valid
object identi er, otherwise, this call will respond with an error message.

curl -i \
-X PUT "http://127.0.0.1:8080/api/blog/categories/[uuid]/" \
-H "Content-Type: application/json" \
-d '{"title": "Dolor sit amet"}'

If everything was ne you should get back the updated details of the category.

PATCH API ENDPOINT


The second input method that we can use to alter an existing model is called a patch. The
main di erence between an update and a patch is that when you perform an update you
have to provide all the elds of a given model, but if you use a patch method for updating a
model you can update one or more elds. In other words, you just update those elds that
you provide, everything else is optional. This is what the Patch object looks like for the
category.

/// FILE: Sources/App/Modules/Blog/Objects/BlogCategory.swift

import Foundation

extension Blog.Category {

// ...

struct Patch: Codable {


let title: String?
}
}

Inside the controller, we create the same methods for the patch functionality that we've
already made for the update and the create features. This time inside the patchInput
method we're going to fall back to the actual model value if the input value was missing. The
best way to do this in Swift is using the nil coalescing operator (??).

/// FILE: Sources/App/Modules/Blog/Controllers/BlogCategoryApiController.swift

import Vapor

extension Blog.Category.List: Content {}


extension Blog.Category.Detail: Content {}

struct BlogCategoryApiController: ModelController {

// ...

func patchInput(
_ req: Request,
_ model: BlogCategoryModel,
_ input: Blog.Category.Patch
) async throws {
model.title = input.title ?? model.title
}

func patchApi(
_ req: Request
) async throws -> Response {

193
ff
fi
fi
fi
fi
fi
let model = try await findBy(identifier(req), on: req.db)
let input = try req.content.decode(Blog.Category.Patch.self)
try await patchInput(req, model, input)
try await model.update(on: req.db)
return try await detailOutput(req, model)
.encodeResponse(for: req)
}
}

We also have to register this new route before we can try out the patch endpoint.

/// FILE: Sources/App/Modules/Blog/BlogRouter.swift

import Vapor

struct BlogRouter: RouteCollection {

let controller = BlogFrontendController()


let postAdminController = BlogPostAdminController()
let categoryAdminController = BlogCategoryAdminController()
let categoryApiController = BlogCategoryApiController()

func boot(routes: RoutesBuilder) throws {

// ...

let blogApi = routes.grouped("api", "blog")


let categoriesApi = blogApi.grouped("categories")
categoriesApi.get(use: categoryApiController.listApi)
let categoryApiId = categoriesApi.grouped(":categoryId")
categoryApiId.get(use: categoryApiController.detailApi)
categoriesApi.post(use: categoryApiController.createApi)
categoryApiId.put(use: categoryApiController.updateApi)
categoryApiId.patch(use: categoryApiController.patchApi)
}
}

You can copy the unique identi er from the previous update request to test this endpoint.

curl -i \
-X PATCH "http://127.0.0.1:8080/api/blog/categories/[uuid]/" \
-H "Content-Type: application/json" \
-d '{"title": "Lorem ipsum dolor sit amet"}'

Hopefully, the patch request will work just like the others, please note that currently these
endpoints aren't validated at all, so if you mess up the input that might lead to unexpected
results.

DELETE API ENDPOINT


Delete is just a little bit tricky but in a good way. Since we don't need a DTO representation
at all, we're going to return an HTTP status code after a successful deletion event.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogCategoryApiController.swift

import Vapor

extension Blog.Category.List: Content {}


extension Blog.Category.Detail: Content {}

struct BlogCategoryApiController: ModelController {

194
fi
// ...

func deleteApi(
_ req: Request
) async throws -> HTTPStatus {
let model = try await findBy(identifier(req), on: req.db)
try await model.delete(on: req.db)
return .noContent
}
}

Vapor has a nice HTTPStatus enum for all the available HTTP status codes, you can use this
enum if you want to express something. Add this deleteApi method to the controller and
register the new route handler using a delete method, so we'll be able to remove a record.

/// FILE: Sources/App/Modules/Blog/BlogRouter.swift

import Vapor

struct BlogRouter: RouteCollection {

let controller = BlogFrontendController()


let postAdminController = BlogPostAdminController()
let categoryAdminController = BlogCategoryAdminController()
let categoryApiController = BlogCategoryApiController()

func boot(routes: RoutesBuilder) throws {

// ...

let blogApi = routes.grouped("api", "blog")


let categoriesApi = blogApi.grouped("categories")
categoriesApi.get(use: categoryApiController.listApi)
let categoryApiId = categoriesApi.grouped(":categoryId")
categoryApiId.get(use: categoryApiController.detailApi)
categoriesApi.post(use: categoryApiController.createApi)
categoryApiId.put(use: categoryApiController.updateApi)
categoryApiId.patch(use: categoryApiController.patchApi)
categoryApiId.delete(use: categoryApiController.deleteApi)
}
}

You can try the new endpoint by running this cURL snippet using the command line.

curl -i -X DELETE "http://127.0.0.1:8080/api/blog/categories/[uuid]/"

Congratulations we've just implemented all of the necessary REST API endpoints.

SUMMARY
In this chapter we've learned how to design and build a basic API service layer. We've
managed to implement all kinds of CRUD operations, including patch support. We've also
discovered the di erent types of HTTP methods to implement the RESTful list, detail, create,
update, patch, and delete APIs.

195
ff
196
CHAPTER 12:
BUILDING A GENERIC REST API

This chapter contains useful materials about how to turn our REST API layer into a reusable
generic solution. We're going to de ne common protocols that'll allow us to share some of
the logic between the admin and API controllers. The rst part is going to be all about the
controller updates, but later on in this chapter, we're also going to improve the routing
mechanism by introducing new setup methods for the route handlers.

LIST
Let’s start by creating a new folder we’ll use during this chapter you can use the commands
below to do this:

cd ~/myProject
mkdir -p Sources/App/Modules/Api/Controllers

In the last few chapters, we've generically created admin controllers. This time we'd like to
do the same for our API-related logic, but since both the admin and API controllers will share
some common methods it makes sense to introduce a new layer of abstraction and derive
the admin and API controllers from base protocols.

We're going to build up generic base controllers for the CRUD methods and place these into
the Framework/Controllers folder. We can come up with a new ListController interface and
place the list function there.

/// FILE: Sources/App/Framework/Controllers/ListController.swift

import Vapor

protocol ListController: ModelController {

func list(
_ req: Request
) async throws -> [DatabaseModel]
}

extension ListController {

func list(
_ req: Request
) async throws -> [DatabaseModel] {
try await DatabaseModel
.query(on: req.db)
.all()
}
}

197
fi
fi
Now it's possible to add this protocol as a dependency to the AdminListController and we
can also delete the list function from the admin protocol extension.

/// FILE: Sources/App/Modules/Admin/Controllers/AdminListController.swift

import Vapor

protocol AdminListController: ListController {

func listView(
_ req: Request
) async throws -> Response

func listColumns() -> [ColumnContext]

func listCells(
for model: DatabaseModel
) -> [CellContext]

func listNavigation(
_ req: Request
) -> [LinkContext]

func listBreadcrumbs(
_ req: Request
) -> [LinkContext]

func listContext(
_ req: Request,
_ list: [DatabaseModel]
) -> AdminListPageContext

func listTemplate(
_ req: Request,
_ list: [DatabaseModel]
) -> TemplateRepresentable
}

extension AdminListController {

// Remove the list function from the extension

We can apply the same steps to create a new ApiListController. Both controllers must have
a list function, and now the common ListController can take care of this.

Inside the generic listApi method we can query the models by calling the common list
function, and we can use the listOutput method to convert the generic database models
into a newly introduced generic ListObject content type array.

/// FILE: Sources/App/Modules/Api/Controllers/ApiListController.swift

import Vapor

protocol ApiListController: ListController {


associatedtype ListObject: Content

func listOutput(
_ req: Request,
_ models: [DatabaseModel]
) async throws -> [ListObject]

func listApi(
_ req: Request
) async throws -> [ListObject]
}

198
extension ApiListController {

func listApi(
_ req: Request
) async throws -> [ListObject] {
let models = try await list(req)
return try await listOutput(req, models)
}
}

The exact type of this list object will be determined inside the actual controller struct, we
don't have to explicitly create a type alias here for the ListObject associated type, because
the listOutput function signature already indicates that it's a Blog.Category.List type.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogCategoryApiController.swift

import Vapor

extension Blog.Category.List: Content {}


extension Blog.Category.Detail: Content {}

struct BlogCategoryApiController: ApiListController {

// Remove the listApi method

Now we can customize our list response via the listOutput method, but if you need more
control you can also provide a custom implementation for the list and listApi too.

DETAIL
The detail functionality won't share too much common code just yet; it's going to be an
empty protocol for now that derives from the ModelController protocol, to begin with. The
only function that's required to retrieve model details is the ability to nd a model by a
unique identi er, which is already there, because of the model protocol.

/// FILE: Sources/App/Framework/Controllers/DetailController.swift

import Vapor

protocol DetailController: ModelController {

extension DetailController {

Let's add this conformance to the AdminDetailController; there's no need for other changes.

/// FILE: Sources/App/Modules/Admin/Controllers/AdminDetailController.swift

import Vapor

public protocol AdminDetailController: DetailController {


// ...
}

199
fi
fi
// ...

Create an ApiDetailController just like we did for the lists. Now inside the detailApi we can
call the ndBy method using the identi er to fetch the model, then we can map it to a
DetailObject using the detailOutput function.

/// FILE: Sources/App/Modules/Api/Controllers/ApiDetailController.swift

import Vapor

protocol ApiDetailController: DetailController {


associatedtype DetailObject: Content

func detailOutput(
_ req: Request,
_ model: DatabaseModel
) async throws -> DetailObject

func detailApi(
_ req: Request
) async throws -> DetailObject
}

extension ApiDetailController {

func detailApi(
_ req: Request
) async throws -> DetailObject {
let model = try await findBy(identifier(req), on: req.db)
return try await detailOutput(req, model)
}
}

Inside the BlogCategoryApiController we can add the new protocol conformance, and we
can also delete the original detail API call since the new ApiDetailController will take care of
all the other stu and it'll provide the default implementation.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogCategoryApiController.swift

import Vapor

extension Blog.Category.List: Content {}


extension Blog.Category.Detail: Content {}

struct BlogCategoryApiController:
ApiListController,
ApiDetailController
{
// Remove the detailApi method

This is the same pattern that we had for our list controller, the Swift compiler is smart enough
to nd out the underlying associated type based on the return type of the detail output
function.

200
fi
fi
ff
fi
CREATE
We can move on to the create functionality, in this case again, we start an empty protocol.

/// FILE: Sources/App/Framework/Controllers/CreateController.swift

import Vapor

protocol CreateController: ModelController {

extension CreateController {

We should extend the AdminCreateController with the new CreateController protocol.

/// FILE: Sources/App/Modules/Admin/Controllers/AdminCreateController.swift

import Vapor

protocol AdminCreateController: CreateController {


// ...
}

Finally, we build a generalized version of the create API functionality. We'll need a
CreateObject associated type, which should be Decodable since we're going to try to
decode this object using the req.content.decode function. Afterward, we can create the new
model and apply the input to the newly initiated database entity.

/// FILE: Sources/App/Modules/Api/Controllers/ApiCreateController.swift

import Vapor

protocol ApiCreateController: CreateController {


associatedtype CreateObject: Decodable

func createInput(
_ req: Request,
_ model: DatabaseModel,
_ input: CreateObject
) async throws

func createApi(
_ req: Request
) async throws -> Response

func createResponse(
_ req: Request,
_ model: DatabaseModel
) async throws -> Response
}

extension ApiCreateController {

func createApi(
_ req: Request
) async throws -> Response {
let input = try req.content.decode(CreateObject.self)
let model = DatabaseModel()
try await createInput(req, model, input)
try await model.create(on: req.db)
return try await createResponse(req, model)

201
}
}

We have one more additional protocol method here, which is called createResponse; it's
going to allow us to decouple things by letting developers provide a custom response after
the model is persisted in the database. A few sections later you'll see how to provide a
generic default implementation for this method, but for now, we're going to place it in the
category controller.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogCategoryApiController.swift

import Vapor

extension Blog.Category.List: Content {}


extension Blog.Category.Detail: Content {}

struct BlogCategoryApiController:
ApiListController,
ApiDetailController,
ApiCreateController
{
typealias DatabaseModel = BlogCategoryModel

var modelName: Name = .init(


singular: "category",
plural: "categories"
)
var parameterId: String = "categoryId"

func listOutput(
_ req: Request,
_ models: [BlogCategoryModel]
) async throws -> [Blog.Category.List] {
models.map {
.init(id: $0.id!, title: $0.title)
}
}

func detailOutput(
_ req: Request,
_ model: BlogCategoryModel
) async throws -> Blog.Category.Detail {
.init(
id: model.id!,
title: model.title
)
}

func createInput(
_ req: Request,
_ model: BlogCategoryModel,
_ input: Blog.Category.Create
) async throws {
model.title = input.title
}

func createResponse(
_ req: Request,
_ model: BlogCategoryModel
) async throws -> Response {
try await detailOutput(req, model)
.encodeResponse(status: .created, for: req)
}

// ...
}

That's it, we've just replaced the entire create functionality with a better, reusable solution.

202
UPDATE
The common update protocol is going to be an empty protocol derived from the model
controller.

/// FILE: Sources/App/Framework/Controllers/UpdateController.swift

import Vapor

public protocol UpdateController: ModelController {

public extension UpdateController {

Again, we extend the AdminUpdateController, to conform to the update controller interface.

/// FILE: Sources/App/Modules/Admin/Controllers/AdminUpdateController.swift

import Vapor

protocol AdminUpdateController: UpdateController {


// ...
}

// ...

Next, we can come up with the generic version of the update functionality. If you want to
challenge yourself, you can try to gure out these details all by yourself; it should be pretty
straightforward.

/// FILE: Sources/App/Modules/Api/Controllers/ApiUpdateController.swift

import Vapor

public protocol ApiUpdateController: UpdateController {


associatedtype UpdateObject: Decodable

func updateInput(
_ req: Request,
_ model: DatabaseModel,
_ input: UpdateObject
) async throws

func updateApi(
_ req: Request
) async throws -> Response

func updateResponse(
_ req: Request,
_ model: DatabaseModel
) async throws -> Response
}

public extension ApiUpdateController {

203
fi
func updateApi(
_ req: Request
) async throws -> Response {
let model = try await findBy(identifier(req), on: req.db)
let input = try req.content.decode(UpdateObject.self)
try await updateInput(req, model, input)
try await model.update(on: req.db)
return try await updateResponse(req, model)
}
}

Follow the same principles and apply the new protocol to the blog category API struct.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogCategoryApiController.swift

import Vapor

extension Blog.Category.List: Content {}


extension Blog.Category.Detail: Content {}

struct BlogCategoryApiController:
ApiListController,
ApiDetailController,
ApiCreateController,
ApiUpdateController
{
typealias DatabaseModel = BlogCategoryModel

var modelName: Name = .init(


singular: "category",
plural: "categories"
)
var parameterId: String = "categoryId"

func listOutput(
_ req: Request,
_ models: [BlogCategoryModel]
) async throws -> [Blog.Category.List] {
models.map {
.init(id: $0.id!, title: $0.title)
}
}

func detailOutput(
_ req: Request,
_ model: BlogCategoryModel
) async throws -> Blog.Category.Detail {
.init(
id: model.id!,
title: model.title
)
}

func createInput(
_ req: Request,
_ model: BlogCategoryModel,
_ input: Blog.Category.Create
) async throws {
model.title = input.title
}

func createResponse(
_ req: Request,
_ model: BlogCategoryModel
) async throws -> Response {
try await detailOutput(req, model)
.encodeResponse(status: .created, for: req)
}

func updateInput(
_ req: Request,

204
_ model: BlogCategoryModel,
_ input: Blog.Category.Update
) async throws {
model.title = input.title
}

func updateResponse(
_ req: Request,
_ model: DatabaseModel
) async throws -> Response {
try await detailOutput(req, model)
.encodeResponse(for: req)
}

// ...
}

As you can see, this is a very common pattern. Fortunately, there aren't many CRUD
functions left to do, but we have to work on the patch and delete endpoints.

PATCH
The patch controller is going to be just a bit di erent because there won't be a patch admin
controller: we can only update our models through the CMS.

/// FILE: Sources/App/Framework/Controllers/PatchController.swift

import Vapor

public protocol PatchController: ModelController {

public extension PatchController {

The ApiPatchController looks almost identical to the update API controller.

/// FILE: Sources/App/Modules/Api/Controllers/ApiPatchController.swift

import Vapor

public protocol ApiPatchController: PatchController {


associatedtype PatchObject: Decodable

func patchInput(
_ req: Request,
_ model: DatabaseModel,
_ input: PatchObject
) async throws

func patchApi(
_ req: Request
) async throws -> Response

func patchResponse(
_ req: Request,
_ model: DatabaseModel
) async throws -> Response
}

public extension ApiPatchController {

205
ff
func patchApi(
_ req: Request
) async throws -> Response {
let model = try await findBy(identifier(req), on: req.db)
let input = try req.content.decode(PatchObject.self)
try await patchInput(req, model, input)
try await model.update(on: req.db)
return try await patchResponse(req, model)
}
}

As usual, the last step is to alter the BlogCategoryApiController with the new patch API.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogCategoryApiController.swift

import Vapor

extension Blog.Category.List: Content {}


extension Blog.Category.Detail: Content {}

struct BlogCategoryApiController:
ApiListController,
ApiDetailController,
ApiCreateController,
ApiUpdateController,
ApiPatchController
{
typealias DatabaseModel = BlogCategoryModel

var modelName: Name = .init(


singular: "category",
plural: "categories"
)
var parameterId: String = "categoryId"

func listOutput(
_ req: Request,
_ models: [BlogCategoryModel]
) async throws -> [Blog.Category.List] {
models.map {
.init(id: $0.id!, title: $0.title)
}
}

func detailOutput(
_ req: Request,
_ model: BlogCategoryModel
) async throws -> Blog.Category.Detail {
.init(
id: model.id!,
title: model.title
)
}

func createInput(
_ req: Request,
_ model: BlogCategoryModel,
_ input: Blog.Category.Create
) async throws {
model.title = input.title
}

func createResponse(
_ req: Request,
_ model: BlogCategoryModel
) async throws -> Response {
try await detailOutput(req, model)
.encodeResponse(status: .created, for: req)
}

func updateInput(

206
_ req: Request,
_ model: BlogCategoryModel,
_ input: Blog.Category.Update
) async throws {
model.title = input.title
}

func updateResponse(
_ req: Request,
_ model: DatabaseModel
) async throws -> Response {
try await detailOutput(req, model)
.encodeResponse(for: req)
}

func patchInput(
_ req: Request,
_ model: BlogCategoryModel,
_ input: Blog.Category.Patch
) async throws {
model.title = input.title ?? model.title
}

func patchResponse(
_ req: Request,
_ model: DatabaseModel
) async throws -> Response {
try await detailOutput(req, model)
.encodeResponse(for: req)
}

// ...

Now it's possible to easily provide the patch endpoint for other API controllers.

DELETE
The shared DeleteController will be just an empty protocol for now.

/// FILE: Sources/App/Framework/Controllers/DeleteController.swift

import Vapor

public protocol DeleteController: ModelController {

public extension DeleteController {

We should update the AdminDeleteController to take advantage of the shared controller


later on.

/// FILE: Sources/App/Modules/Admin/Controllers/AdminDeleteController.swift

import Vapor

final class DeleteForm: AbstractForm {

init() {

207
super.init()

self.action.method = .post
self.submit = "Delete"
}
}

protocol AdminDeleteController: DeleteController {

// ...
}

We can introduce one more API controller, the delete functionality can be moved there
without major changes.

/// FILE: Sources/App/Modules/Api/Controllers/ApiDeleteController.swift

import Vapor

public protocol ApiDeleteController: DeleteController {

func deleteApi(
_ req: Request
) async throws -> HTTPStatus
}

public extension ApiDeleteController {

func deleteApi(
_ req: Request
) async throws -> HTTPStatus {
let model = try await findBy(identifier(req), on: req.db)
try await model.delete(on: req.db)
return .noContent
}
}

Now that we're ready with all the shared CRUD protocols and the API controller protocols,
this is what the nal BlogCategoryApiController should look like.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogCategoryApiController.swift

import Vapor

extension Blog.Category.List: Content {}


extension Blog.Category.Detail: Content {}

struct BlogCategoryApiController:
ApiListController,
ApiDetailController,
ApiCreateController,
ApiUpdateController,
ApiPatchController,
ApiDeleteController
{
typealias DatabaseModel = BlogCategoryModel

var modelName: Name = .init(


singular: "category",
plural: "categories"
)
var parameterId: String = "categoryId"

func listOutput(
_ req: Request,
_ models: [BlogCategoryModel]
) async throws -> [Blog.Category.List] {
models.map {
.init(id: $0.id!, title: $0.title)

208
fi
}
}

func detailOutput(
_ req: Request,
_ model: BlogCategoryModel
) async throws -> Blog.Category.Detail {
.init(
id: model.id!,
title: model.title
)
}

func createInput(
_ req: Request,
_ model: BlogCategoryModel,
_ input: Blog.Category.Create
) async throws {
model.title = input.title
}

func createResponse(
_ req: Request,
_ model: BlogCategoryModel
) async throws -> Response {
try await detailOutput(req, model)
.encodeResponse(status: .created, for: req)
}

func updateInput(
_ req: Request,
_ model: BlogCategoryModel,
_ input: Blog.Category.Update
) async throws {
model.title = input.title
}

func updateResponse(
_ req: Request,
_ model: DatabaseModel
) async throws -> Response {
try await detailOutput(req, model)
.encodeResponse(for: req)
}

func patchInput(
_ req: Request,
_ model: BlogCategoryModel,
_ input: Blog.Category.Patch
) async throws {
model.title = input.title ?? model.title
}

func patchResponse(
_ req: Request,
_ model: DatabaseModel
) async throws -> Response {
try await detailOutput(req, model)
.encodeResponse(for: req)
}
}

It's still quite a lot of ceremony happening here, let's x that real quick.

209
fi
ADMIN AND API CONTROLLER
We can come up with a new AdminController protocol, that'll combine all the CRUD
controllers into a single entity.

/// FILE: Sources/App/Modules/Admin/Controllers/AdminController.swift

import Vapor

protocol AdminController:
AdminListController,
AdminDetailController,
AdminCreateController,
AdminUpdateController,
AdminDeleteController
{

extension AdminController {

We can update the BlogCategoryAdminController to simplify the protocol inheritance.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogCategoryAdminController.swift

import Vapor
import Fluent

struct BlogCategoryAdminController: AdminController {

// ...
}

Same thing goes for the BlogPostAdminController.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogPostAdminController.swift

import Vapor
import Fluent

struct BlogPostAdminController: AdminController {

// ...
}

Next up, we can create an ApiController and now it'll make sense why we've created the
response functions on the protocols. Since the create, update, and patch should work
independently from the detail controller, this way it's possible to maintain a clean code.

Inside the ApiController we can have a default implementation for these protocol methods
because we can be quite sure that the detailOutput method is available since the controller
implements all the other CRUD protocols.

/// FILE: Sources/App/Modules/Api/Controllers/ApiController.swift

import Vapor

protocol ApiController:

210
ApiListController,
ApiDetailController,
ApiCreateController,
ApiUpdateController,
ApiPatchController,
ApiDeleteController
{

extension ApiController {

func createResponse(
_ req: Request,
_ model: DatabaseModel
) async throws -> Response {
try await detailOutput(req, model)
.encodeResponse(status: .created, for: req)
}

func updateResponse(
_ req: Request,
_ model: DatabaseModel
) async throws -> Response {
try await detailOutput(req, model)
.encodeResponse(for: req)
}

func patchResponse(
_ req: Request,
_ model: DatabaseModel
) async throws -> Response {
try await detailOutput(req, model)
.encodeResponse(for: req)
}
}

Now it's possible to remove the unneeded response functions from the blog category API
controller.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogCategoryApiController.swift

import Vapor

extension Blog.Category.List: Content {}


extension Blog.Category.Detail: Content {}

struct BlogCategoryApiController: ApiController {


typealias DatabaseModel = BlogCategoryModel

var modelName: Name = .init(


singular: "category",
plural: "categories"
)
var parameterId: String = "categoryId"

func listOutput(
_ req: Request,
_ models: [BlogCategoryModel]
) async throws -> [Blog.Category.List] {
models.map {
.init(id: $0.id!, title: $0.title)
}
}

func detailOutput(
_ req: Request,
_ model: BlogCategoryModel
) async throws -> Blog.Category.Detail {
.init(

211
id: model.id!,
title: model.title
)
}

func createInput(
_ req: Request,
_ model: BlogCategoryModel,
_ input: Blog.Category.Create
) async throws {
model.title = input.title
}

func updateInput(
_ req: Request,
_ model: BlogCategoryModel,
_ input: Blog.Category.Update
) async throws {
model.title = input.title
}

func patchInput(
_ req: Request,
_ model: BlogCategoryModel,
_ input: Blog.Category.Patch
) async throws {
model.title = input.title ?? model.title
}
}

We're mostly ready with our API, so we can start de ning the blog post data transfer objects.

/// FILE: Sources/App/Modules/Blog/Objects/BlogPost.swift

import Foundation

extension Blog.Post {

struct List: Codable {


let id: UUID
let title: String
let slug: String
let image: String
let excerpt: String
let date: Date
}

struct Detail: Codable {


let id: UUID
let title: String
let slug: String
let image: String
let excerpt: String
let date: Date
let category: Blog.Category.List
let content: String
}

struct Create: Codable {


let title: String
let slug: String
let image: String
let excerpt: String
let date: Date
let content: String
}

struct Update: Codable {


let title: String
let slug: String
let image: String
let excerpt: String

212
fi
let date: Date
let content: String
}

struct Patch: Codable {


let title: String?
let slug: String?
let image: String?
let excerpt: String?
let date: Date?
let content: String?
}
}

It's relatively easy to alter the original BlogPostApiController and use the new set of APIs.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogPostApiController.swift

import Vapor

extension Blog.Post.List: Content {}


extension Blog.Post.Detail: Content {}

struct BlogPostApiController: ApiController {


typealias DatabaseModel = BlogPostModel

var modelName: Name = .init(singular: "post")


var parameterId: String = "postId"

func listOutput(
_ req: Request,
_ models: [DatabaseModel]
) async throws -> [Blog.Post.List] {
models.map { model in
.init(
id: model.id!,
title: model.title,
slug: model.slug,
image: model.imageKey,
excerpt: model.excerpt,
date: model.date
)
}
}

func detailOutput(
_ req: Request,
_ model: DatabaseModel
) async throws -> Blog.Post.Detail {
.init(
id: model.id!,
title: model.title,
slug: model.slug,
image: model.imageKey,
excerpt: model.excerpt,
date: model.date,
category: .init(
id: model.category.id!,
title: model.category.title
),
content: model.content
)
}

func createInput(
_ req: Request,
_ model: DatabaseModel,
_ input: Blog.Post.Create
) async throws {
model.title = input.title
model.slug = input.slug
model.imageKey = input.image

213
model.excerpt = input.excerpt
model.date = input.date
model.content = input.content
}

func updateInput(
_ req: Request,
_ model: DatabaseModel,
_ input: Blog.Post.Update
) async throws {
model.title = input.title
model.slug = input.slug
model.imageKey = input.image
model.excerpt = input.excerpt
model.date = input.date
model.content = input.content
}

func patchInput(
_ req: Request,
_ model: DatabaseModel,
_ input: Blog.Post.Patch
) async throws {
model.title = input.title ?? model.title
model.slug = input.slug ?? model.slug
model.imageKey = input.image ?? model.imageKey
model.excerpt = input.excerpt ?? model.excerpt
model.date = input.date ?? model.date
model.content = input.content ?? model.content
}
}

Finally we also have to align the BlogFrontendController, instead of the original mapper
functions we can use the list and detail output to convert our models to DTOs.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogFrontendController.swift

import Vapor
import Fluent

struct BlogFrontendController {

func blogView(req: Request) async throws -> Response {


let posts = try await BlogPostModel
.query(on: req.db)
.sort(\.$date, .descending)
.all()

let api = BlogPostApiController()


let listOutput = try await api.listOutput(req, posts)

let ctx = BlogPostsContext(


icon: "🔥 ",
title: "Blog",
message: "Hot news and stories about everything.",
posts: listOutput
)

return req.templates.renderHtml(BlogPostsTemplate(ctx))
}

func postView(req: Request) async throws -> Response {


let slug = req.url.path.trimmingCharacters(
in: .init(charactersIn: "/")
)
guard
let post = try await BlogPostModel
.query(on: req.db)
.filter(\.$slug == slug)
.with(\.$category)
.first()

214
else {
return req.redirect(to: "/")
}
let api = BlogPostApiController()
let postOutput = try await api.detailOutput(req, post)
let ctx = BlogPostContext(post: postOutput)
return req.templates.renderHtml(BlogPostTemplate(ctx))
}
}

After you make these changes, the project should work, but the post API routes aren't yet
registered, so don't try to use them just yet. We still have to x a few more things before we
enable those.

NAMES AND PATH KEYS


In the future we're planning to share Data Transfer Objects with other Swift clients, so we
can reuse all the object de nitions, this way we can have a type-safe backend API.

The very rst step to achieve this is a new ApiModelInterface protocol that we can also
share with the frontend clients. This interface will contain the pathIdKey used to get back id
parameters on the server-side, but it can be also useful information for the frontends.

/// FILE: Sources/App/Framework/ApiModelInterface.swift

import Foundation

public protocol ApiModelInterface {


static var pathIdKey: String { get }
}

extension ApiModelInterface {

static var pathIdKey: String {


String(describing: self).lowercased() + "Id"
}
}

Inside the ModelController we're going to use this ApiModelInterface as an associated


generic type, this way we can use the pathIdKey to get back the model identi er.

We're going to introduce a new static moduleName variable and we can turn the
modelName into a static variable as well. We also provide a default implementation for these
properties.

Now we should remove the original parameterId variable from the ModelController
protocol.

You can also remove the parameterId from the corresponding blog and category admin
controllers.

/// FILE: Sources/App/Framework/Controllers/ModelController.swift

import Vapor
import Fluent

215
fi
fi
fi
fi
public struct Name {

let singular: String


let plural: String

init(
singular: String,
plural: String? = nil
) {
self.singular = singular
self.plural = plural ?? singular + "s"
}
}

public protocol ModelController {


associatedtype ApiModel: ApiModelInterface
associatedtype DatabaseModel: DatabaseModelInterface

static var moduleName: String { get }


static var modelName: Name { get }

func identifier(
_ req: Request
) throws -> UUID

func findBy(
_ id: UUID,
on: Database
) async throws -> DatabaseModel
}

extension ModelController {

static var moduleName: String {


DatabaseModel.Module.identifier.capitalized
}

static var modelName: Name {


.init(singular: String(DatabaseModel.identifier.dropLast(1)))
}

func identifier(
_ req: Request
) throws -> UUID {
guard
let id = req.parameters.get(ApiModel.pathIdKey),
let uuid = UUID(uuidString: id)
else {
throw Abort(.badRequest)
}
return uuid
}

func findBy(
_ id: UUID,
on db: Database
) async throws -> DatabaseModel {
guard let model = try await DatabaseModel.find(id, on: db) else {
throw Abort(.notFound)
}
return model
}
}

Inside the admin detail controller now it's possible to update the breadcrumbs by using the
new static name variables.

/// FILE: Sources/App/Modules/Admin/Controllers/AdminDetailController.swift

import Vapor

216
// ...

extension AdminDetailController {

// ...

func detailBreadcrumbs(
_ req: Request,
_ model: DatabaseModel
) -> [LinkContext] {
[
LinkContext(
label: Self.moduleName.capitalized,
dropLast: 2
),
LinkContext(
label: Self.modelName.plural.capitalized,
dropLast: 1
),
]
}

// ...
}

We should also use these inside the create controller.

/// FILE: Sources/App/Modules/Admin/Controllers/AdminCreateController.swift

import Vapor

// ...

extension AdminCreateController {

// ...

func createBreadcrumbs(
_ req: Request
) -> [LinkContext] {
[
LinkContext(
label: Self.moduleName.capitalized,
dropLast: 2
),
LinkContext(
label: Self.modelName.plural.capitalized,
dropLast: 1
),
]
}
}

Finally, apply the names to the update controller as well.

/// FILE: Sources/App/Modules/Admin/Controllers/AdminUpdateController.swift

import Vapor

// ...

extension AdminUpdateController {

// ...

func updateBreadcrumbs(
_ req: Request,
_ model: DatabaseModel

217
) -> [LinkContext] {
[
LinkContext(
label: Self.moduleName.capitalized,
dropLast: 3
),
LinkContext(
label: Self.modelName.plural.capitalized,
dropLast: 2
),
]
}

// ...
}

Now inside the Objects folder, we should change the Post object to conform to the
ApiModelInterface. The blog Category object should also implement the model API
interface.

/// FILE: Sources/App/Modules/Blog/Objects/Blog.swift

enum Blog {

enum Post: ApiModelInterface {

enum Category: ApiModelInterface {

}
}

The blog post API controller requires the ApiModel type alias, we can use the Blog.Post
object for this purpose, because it already conforms to the ApiModelInterface. You can also
remove the modelName and parameterId properties.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogPostApiController.swift

import Vapor

extension Blog.Post.List: Content {}


extension Blog.Post.Detail: Content {}

struct BlogPostApiController: ApiController {


typealias ApiModel = Blog.Post
typealias DatabaseModel = BlogPostModel

// ...
}

The category API controller needs to be changed too.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogCategoryApiController.swift

import Vapor

extension Blog.Category.List: Content {}


extension Blog.Category.Detail: Content {}

struct BlogCategoryApiController: ApiController {


typealias ApiModel = Blog.Category
typealias DatabaseModel = BlogCategoryModel

// ...

218
}

Because the ModelController has this new generic requirement, we also have to set the
ApiModel inside the admin controllers.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogPostAdminController.swift

import Vapor
import Fluent

struct BlogPostAdminController: AdminController {


typealias ApiModel = Blog.Post
typealias DatabaseModel = BlogPostModel
typealias CreateModelEditor = BlogPostEditor
typealias UpdateModelEditor = BlogPostEditor

// ...
}

Again, in the BlogCategoryAdminController we have to de ne the ApiModel type alias.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogCategoryAdminController.swift

import Vapor
import Fluent

struct BlogCategoryAdminController: AdminController {


typealias ApiModel = Blog.Category
typealias DatabaseModel = BlogCategoryModel
typealias CreateModelEditor = BlogCategoryEditor
typealias UpdateModelEditor = BlogCategoryEditor

// ...
}

We're one step closer to a more reusable API layer that can be shared with other clients.

A BETTER ROUTING SYSTEM


To have a nice routing system, we'll have to introduce one more new protocol. Since most of
our paths follow the /frame/module/model/id/ pattern, we should de ne the path key for the
modules using an ApiModuleInterface protocol.

/// FILE: Sources/App/Framework/ApiModuleInterface.swift

import Foundation

public protocol ApiModuleInterface {


static var pathKey: String { get }
}

public extension ApiModuleInterface {

static var pathKey: String {


String(describing: self).lowercased()
}
}

219
fi
fi
We also have to de ne the pathKeys for the models, plus it would be nice to associate the
model with a given module, so we can alter the ApiModelInterface to add this new feature.

/// FILE: Sources/App/Framework/ApiModelInterface.swift

import Vapor

public protocol ApiModelInterface {


associatedtype Module: ApiModuleInterface

static var pathKey: String { get }


static var pathIdKey: String { get }
}

extension ApiModelInterface {

static var pathKey: String {


String(describing: self).lowercased() + "s"
}

static var pathIdKey: String {


String(describing: self).lowercased() + "Id"
}

static var pathIdComponent: PathComponent {


.init(stringLiteral: ":" + pathIdKey)
}
}

Next, we should change the Blog enum to conform to the ApiModuleInterface protocol.
Since the category word has an irregular plural form, we explicitly set the pathKey and the
Module type alias. We just have to setup the module association inside the blog Post
namespace.

/// FILE: Sources/App/Modules/Blog/Objects/Blog.swift

enum Blog: ApiModuleInterface {

enum Post: ApiModelInterface {


typealias Module = Blog
}

enum Category: ApiModelInterface {


typealias Module = Blog

static let pathKey: String = "categories"


}
}

It's possible to take advantage of the newly added module and model path keys, we can add
a new getBaseRoutes method to the ModelController protocol, this way other controllers
can append new routes to the base route which is going to use the module and model
paths.

/// FILE: Sources/App/Framework/Controllers/ModelController.swift

import Vapor
import Fluent

public struct Name {

let singular: String


let plural: String

init(
singular: String,

220
fi
plural: String? = nil
) {
self.singular = singular
self.plural = plural ?? singular + "s"
}
}

public protocol ModelController {


associatedtype ApiModel: ApiModelInterface
associatedtype DatabaseModel: DatabaseModelInterface

static var moduleName: String { get }


static var modelName: Name { get }

func identifier(
_ req: Request
) throws -> UUID

func findBy(
_ id: UUID,
on: Database
) async throws -> DatabaseModel

func getBaseRoutes(
_ routes: RoutesBuilder
) -> RoutesBuilder
}

extension ModelController {

static var moduleName: String {


DatabaseModel.Module.identifier.capitalized
}

static var modelName: Name {


.init(singular: String(DatabaseModel.identifier.dropLast(1)))
}

func identifier(
_ req: Request
) throws -> UUID {
guard
let id = req.parameters.get(ApiModel.pathIdKey),
let uuid = UUID(uuidString: id)
else {
throw Abort(.badRequest)
}
return uuid
}

func findBy(
_ id: UUID,
on db: Database
) async throws -> DatabaseModel {
guard let model = try await DatabaseModel.find(id, on: db) else {
throw Abort(.notFound)
}
return model
}

func getBaseRoutes(
_ routes: RoutesBuilder
) -> RoutesBuilder {
routes
.grouped(ApiModel.Module.pathKey.pathComponents)
.grouped(ApiModel.pathKey.pathComponents)
}
}

For example, this is how you can set up the list routes inside the ApiListController.

/// FILE: Sources/App/Modules/Api/Controllers/ApiListController.swift

221
import Vapor

protocol ApiListController: ListController {


associatedtype ListObject: Content

func listOutput(
_ req: Request,
_ models: [DatabaseModel]
) async throws -> [ListObject]

func listApi(
_ req: Request
) async throws -> [ListObject]

func setupListRoutes(
_ routes: RoutesBuilder
)
}

extension ApiListController {

func listApi(
_ req: Request
) async throws -> [ListObject] {
let models = try await list(req)
return try await listOutput(req, models)
}

func setupListRoutes(
_ routes: RoutesBuilder
) {
let baseRoutes = getBaseRoutes(routes)
baseRoutes.get(use: listApi)
}
}

Inside the ApiDetailController we can use the pathIdComponent property of the generic
ApiModel so it's possible to create a new group and append the get handler to that route.

/// FILE: Sources/App/Modules/Api/Controllers/ApiDetailController.swift

import Vapor

protocol ApiDetailController: DetailController {


associatedtype DetailObject: Content

func detailOutput(
_ req: Request,
_ model: DatabaseModel
) async throws -> DetailObject

func detailApi(
_ req: Request
) async throws -> DetailObject

func setupDetailRoutes(
_ routes: RoutesBuilder
)
}

extension ApiDetailController {

func detailApi(
_ req: Request
) async throws -> DetailObject {
let model = try await findBy(identifier(req), on: req.db)
return try await detailOutput(req, model)
}

func setupDetailRoutes(

222
_ routes: RoutesBuilder
) {
let baseRoutes = getBaseRoutes(routes)
let existingModelRoutes = baseRoutes
.grouped(ApiModel.pathIdComponent)

existingModelRoutes.get(use: detailApi)
}

The create controller follows the pattern that we had in the list controller.

/// FILE: Sources/App/Modules/Api/Controllers/ApiCreateController.swift

import Vapor

protocol ApiCreateController: CreateController {


associatedtype CreateObject: Decodable

func createInput(
_ req: Request,
_ model: DatabaseModel,
_ input: CreateObject
) async throws

func createApi(
_ req: Request
) async throws -> Response

func createResponse(
_ req: Request,
_ model: DatabaseModel
) async throws -> Response

func setupCreateRoutes(
_ routes: RoutesBuilder
)
}

extension ApiCreateController {

func createApi(
_ req: Request
) async throws -> Response {
let input = try req.content.decode(CreateObject.self)
let model = DatabaseModel()
try await createInput(req, model, input)
try await model.create(on: req.db)
return try await createResponse(req, model)
}

func setupCreateRoutes(
_ routes: RoutesBuilder
) {
let baseRoutes = getBaseRoutes(routes)
baseRoutes.post(use: createApi)
}
}

The update controller looks like the detail controller.

/// FILE: Sources/App/Modules/Api/Controllers/ApiUpdateController.swift

import Vapor

public protocol ApiUpdateController: UpdateController {


associatedtype UpdateObject: Decodable

func updateInput(

223
_ req: Request,
_ model: DatabaseModel,
_ input: UpdateObject
) async throws

func updateApi(
_ req: Request
) async throws -> Response

func updateResponse(
_ req: Request,
_ model: DatabaseModel
) async throws -> Response

func setupUpdateRoutes(
_ routes: RoutesBuilder
)
}

public extension ApiUpdateController {

func updateApi(
_ req: Request
) async throws -> Response {
let model = try await findBy(identifier(req), on: req.db)
let input = try req.content.decode(UpdateObject.self)
try await updateInput(req, model, input)
try await model.update(on: req.db)
return try await updateResponse(req, model)
}

func setupUpdateRoutes(
_ routes: RoutesBuilder
) {
let baseRoutes = getBaseRoutes(routes)
let existingModelRoutes = baseRoutes
.grouped(ApiModel.pathIdComponent)

existingModelRoutes.put(use: updateApi)
}
}

The patch controller looks very similar to the update, but be careful and always use the right
HTTP method (PATCH).

/// FILE: Sources/App/Modules/Api/Controllers/ApiPatchController.swift

import Vapor

public protocol ApiPatchController: PatchController {


associatedtype PatchObject: Decodable

func patchInput(
_ req: Request,
_ model: DatabaseModel,
_ input: PatchObject
) async throws

func patchApi(
_ req: Request
) async throws -> Response

func patchResponse(
_ req: Request,
_ model: DatabaseModel
) async throws -> Response

func setupPatchRoutes(
_ routes: RoutesBuilder
)
}

224
public extension ApiPatchController {

func patchApi(
_ req: Request
) async throws -> Response {
let model = try await findBy(identifier(req), on: req.db)
let input = try req.content.decode(PatchObject.self)
try await patchInput(req, model, input)
try await model.update(on: req.db)
return try await patchResponse(req, model)
}

func setupPatchRoutes(
_ routes: RoutesBuilder
) {
let baseRoutes = getBaseRoutes(routes)
let existingModelRoutes = baseRoutes
.grouped(ApiModel.pathIdComponent)

existingModelRoutes.patch(use: patchApi)
}
}

The setupDeleteRoutes method follows the style of the setup detail routes function.

/// FILE: Sources/App/Modules/Api/Controllers/ApiDeleteController.swift

import Vapor

public protocol ApiDeleteController: DeleteController {

func deleteApi(
_ req: Request
) async throws -> HTTPStatus

func setupDeleteRoutes(
_ routes: RoutesBuilder
)
}

public extension ApiDeleteController {

func deleteApi(
_ req: Request
) async throws -> HTTPStatus {
let model = try await findBy(identifier(req), on: req.db)
try await model.delete(on: req.db)
return .noContent
}

func setupDeleteRoutes(
_ routes: RoutesBuilder
) {
let baseRoutes = getBaseRoutes(routes)
let existingModelRoutes = baseRoutes
.grouped(ApiModel.pathIdComponent)

existingModelRoutes.delete(use: deleteApi)
}
}

Finally, before we start working on the admin controllers, we can extend the ApiController
and call each setup route method inside a common setupRoutes function.

/// FILE: Sources/App/Modules/Api/Controllers/ApiController.swift

import Vapor

225
protocol ApiController:
ApiListController,
ApiDetailController,
ApiCreateController,
ApiUpdateController,
ApiPatchController,
ApiDeleteController
{
func setupRoutes(
_ routes: RoutesBuilder
)
}

extension ApiController {

func createResponse(
_ req: Request,
_ model: DatabaseModel
) async throws -> Response {
try await detailOutput(req, model)
.encodeResponse(status: .created, for: req)
}

func updateResponse(
_ req: Request,
_ model: DatabaseModel
) async throws -> Response {
try await detailOutput(req, model)
.encodeResponse(for: req)
}

func patchResponse(
_ req: Request,
_ model: DatabaseModel
) async throws -> Response {
try await detailOutput(req, model)
.encodeResponse(for: req)
}

func setupRoutes(
_ routes: RoutesBuilder
) {
setupListRoutes(routes)
setupDetailRoutes(routes)
setupCreateRoutes(routes)
setupUpdateRoutes(routes)
setupPatchRoutes(routes)
setupDeleteRoutes(routes)
}
}

The following snippets will be quite boring since we have to repeat the same process for all
the admin controllers.

/// FILE: Sources/App/Modules/Admin/Controllers/AdminListController.swift

import Vapor

protocol AdminListController: ListController {

// ...
func setupListRoutes(
_ routes: RoutesBuilder
)
}

extension AdminListController {
// ...

func setupListRoutes(
_ routes: RoutesBuilder

226
) {
let baseRoutes = getBaseRoutes(routes)

baseRoutes.get(use: listView)
}
}

The setup routes for the list and detail controllers are trivial to implement.

/// FILE: Sources/App/Modules/Admin/Controllers/AdminDetailController.swift

import Vapor

protocol AdminDetailController: DetailController {

// ...
func setupDetailRoutes(
_ routes: RoutesBuilder
)
}

extension AdminDetailController {

// ...

func setupDetailRoutes(
_ routes: RoutesBuilder
) {
let baseRoutes = getBaseRoutes(routes)

let existingModelRoutes = baseRoutes


.grouped(ApiModel.pathIdComponent)

existingModelRoutes.get(use: detailView)
}
}

Inside the create controller we're going to hardcode the create word, but maybe it's a good
practice to de ne a new variable or input parameter, for this purpose so developers could
have more control over naming routes. Anyway, we won't do that now, but it's worth
mentioning.

/// FILE: Sources/App/Modules/Admin/Controllers/AdminCreateController.swift

import Vapor

protocol AdminCreateController: CreateController {


// ...
func setupCreateRoutes(
_ routes: RoutesBuilder
)
}

extension AdminCreateController {

// ...

func setupCreateRoutes(
_ routes: RoutesBuilder
) {
let baseRoutes = getBaseRoutes(routes)
baseRoutes.get("create", use: createView)
baseRoutes.post("create", use: createAction)
}
}

227
fi
The same thing applies to the update controller, don't forget browsers can only send GET
and POST requests by default, that's why we won't use other HTTP verbs for admin
controllers.

/// FILE: Sources/App/Modules/Admin/Controllers/AdminUpdateController.swift

import Vapor

protocol AdminUpdateController: UpdateController {


// ...
func setupUpdateRoutes(
_ routes: RoutesBuilder
)
}

extension AdminUpdateController {

// ...

func setupUpdateRoutes(
_ routes: RoutesBuilder
) {
let baseRoutes = getBaseRoutes(routes)

let existingModelRoutes = baseRoutes


.grouped(ApiModel.pathIdComponent)

existingModelRoutes.get("update", use: updateView)


existingModelRoutes.post("update", use: updateAction)
}
}

We still have to update the delete controller.

/// FILE: Sources/App/Modules/Admin/Controllers/AdminDeleteController.swift

import Vapor

// ...

protocol AdminDeleteController: DeleteController {

// ...
func setupDeleteRoutes(
_ routes: RoutesBuilder
)
}

extension AdminDeleteController {

// ...
func setupDeleteRoutes(
_ routes: RoutesBuilder
) {
let baseRoutes = getBaseRoutes(routes)

let existingModelRoutes = baseRoutes


.grouped(ApiModel.pathIdComponent)

existingModelRoutes.get("delete", use: deleteView)


existingModelRoutes.post("delete", use: deleteAction)
}
}

Lastly, just like for the ApiController, we can change the AdminController and provide a
setupRoutes method that can take care of all the underlying CRUD routes.

228
/// FILE: Sources/App/Modules/Admin/Controllers/AdminController.swift

import Vapor

protocol AdminController:
AdminListController,
AdminDetailController,
AdminCreateController,
AdminUpdateController,
AdminDeleteController
{
func setupRoutes(
_ routes: RoutesBuilder
)
}

extension AdminController {

func setupRoutes(
_ routes: RoutesBuilder
) {
setupListRoutes(routes)
setupDetailRoutes(routes)
setupCreateRoutes(routes)
setupUpdateRoutes(routes)
setupDeleteRoutes(routes)
}
}

As a very last step in this chapter, now we should update the BlogRouter and register all the
necessary route handlers using the new setup routes on the controllers.

/// FILE: Sources/App/Modules/Blog/BlogRouter.swift

import Vapor

struct BlogRouter: RouteCollection {

let frontendController = BlogFrontendController()

let postAdminController = BlogPostAdminController()


let postApiController = BlogPostApiController()

let categoryAdminController = BlogCategoryAdminController()


let categoryApiController = BlogCategoryApiController()

func boot(routes: RoutesBuilder) throws {


routes.get("blog", use: frontendController.blogView)
routes.get(.anything, use: frontendController.postView)

let admin = routes


.grouped(
AuthenticatedUser.redirectMiddleware(
path: "/"
)
)
.grouped("admin")

postAdminController.setupRoutes(admin)
categoryAdminController.setupRoutes(admin)

let api = routes.grouped("api")


postApiController.setupRoutes(api)
categoryApiController.setupRoutes(api)
}
}

229
That's much better if you compare the original router le to this one. You can see why it's a
better practice to have these setup route methods in the rst place. The router looks clean
and it's way better organized than it was before.

SUMMARY
In this chapter we've learned how to share some of the common logic between the admin
and the API controllers. Swift protocols and extensions are extremely powerful, that's why we
built this system using a protocol-oriented generic approach. It's also useful to move the
routing into the DTO layer; this way you can reuse the same path components when
communicating to the backend server. The new routing system also allowed us to remove
most of the duplicated code from the routers.

230
fi
fi
CHAPTER 13:
API PROTECTION AND VALIDATION

This chapter is about making the backend service more secure by introducing better API
protections and validation methods. The rst part is about user authentication through
bearer tokens. We're going to create a new token-based authenticator and guard the API
endpoints against unauthenticated requests. The second part is going to be all about data
validation by using the async validator logic that we've created a few chapters before. In the
very last section of this chapter, we're going to introduce some additional lifecycle methods
for the controllers.

USER AUTHENTICATION USING BEARER TOKENS


Let’s start by creating the new folders we’ll use during this chapter. You can use the
commands below to do this:

cd ~/myProject
mkdir -p Sources/App/Modules/User/Objects
mkdir Sources/App/Modules/Api/Middlewares

The API endpoints are currently accessible by everyone, we allow every visitor to create,
update or even remove objects, and that's a problem. Ideally, we want to put a restriction on
the API endpoints to only allow authenticated users to access them, just as we did for the
admin.

To protect the API we're going to build a simple token-based authentication system, that can
live alongside the other session-based authentication. We've already learned how to use
sessions and user authentication in Chapter 5. Feel free to revisit that chapter now.

Building a token-based authentication system isn't that hard at all. We can simply start with a
new UserTokenModel database entity and exchange the login credentials for these kinds of
tokens.

/// FILE: Sources/App/Modules/User/Database/Models/UserTokenModel.swift

import Vapor
import Fluent

final class UserTokenModel: DatabaseModelInterface {


typealias Module = UserModule

struct FieldKeys {
struct v1 {
static var value: FieldKey { "value" }
static var userId: FieldKey { "user_id" }
}
}

@ID()

231
fi
var id: UUID?

@Field(key: FieldKeys.v1.value)
var value: String

@Parent(key: FieldKeys.v1.userId)
var user: UserAccountModel

init() { }

init(
id: UUID? = nil,
value: String,
userId: UUID
) {
self.id = id
self.value = value
self.$user.id = userId
}
}

We'll also have to change the migration to create the table for the user token objects. If you
decide to alter the v1 script you can simply delete the db.sqlite le sitting in the Resources
folder, alternatively you can create a v2 migration and the auto migration will take care of
everything, it's totally up to you which solution you choose.

/// FILE: Sources/App/Modules/User/Database/Migrations/UserMigrations.swift

import Vapor
import Fluent

enum UserMigrations {

struct v1: AsyncMigration {

func prepare(on db: Database) async throws {


try await db.schema(UserAccountModel.schema)
.id()
.field(UserAccountModel.FieldKeys.v1.email, .string, .required)
.field(UserAccountModel.FieldKeys.v1.password, .string, .required)
.unique(on: UserAccountModel.FieldKeys.v1.email)
.create()

try await db.schema(UserTokenModel.schema)


.id()
.field(UserTokenModel.FieldKeys.v1.value, .string, .required)
.field(UserTokenModel.FieldKeys.v1.userId, .uuid, .required)
.foreignKey(
UserTokenModel.FieldKeys.v1.userId,
references: UserAccountModel.schema, .id
)
.unique(on: UserTokenModel.FieldKeys.v1.value)
.create()
}

func revert(on db: Database) async throws {


try await db.schema(UserTokenModel.schema).delete()
try await db.schema(UserAccountModel.schema).delete()
}
}

// ...

We've already created migrations in the past before, so the migration code should be
familiar by now.

232
fi
Since we haven't created API objects for the user module just yet, it's time to build the
structure that we're going to need for the API endpoints. We can start with the User module.

/// FILE: Sources/App/Modules/User/Objects/User.swift

enum User: ApiModuleInterface {

enum Account: ApiModelInterface {


typealias Module = User
}

enum Token: ApiModelInterface {


typealias Module = User
}
}

The public user account object will represent a user; for now, we won't create REST
endpoints to manage users, but we're going to need the Detail object when we return with a
token.

/// FILE: Sources/App/Modules/User/Objects/UserAccount.swift

import Foundation

extension User.Account {

struct List: Codable {


let id: UUID
let email: String
}

struct Detail: Codable {


let id: UUID
let email: String
}

struct Create: Codable {


let email: String
let password: String
}

struct Update: Codable {


let email: String
let password: String?
}

struct Patch: Codable {


let email: String?
let password: String?
}
}

The API object for the token will only contain a Detail struct with an associated user detail
object, we only going to allow users to get tokens after a successful login attempt.

/// FILE: Sources/App/Modules/User/Objects/UserToken.swift

import Foundation

extension User.Token {

struct Detail: Codable {


let id: UUID
let value: String

233
let user: User.Account.Detail
}
}

Now we can focus on the controller that'll provide the logic for the API login handler.

/// FILE: Sources/App/Modules/User/Controllers/UserApiController.swift

import Vapor

extension User.Token.Detail: Content {}

struct UserApiController {

func signInApi(
req: Request
) async throws -> User.Token.Detail {
guard let user = req.auth.get(AuthenticatedUser.self) else {
throw Abort(.unauthorized)
}

let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789="


let tokenValue = String((0..<64).map { _ in letters.randomElement()! })

let token = UserTokenModel(


value: tokenValue,
userId: user.id
)

try await token.create(on: req.db)

let account = User.Account.Detail(


id: user.id,
email: user.email
)
return .init(
id: token.id!,
value: token.value,
user: account
)
}
}

Inside the sign-in method, we're going to check if there's an AuthenticatedUser in the
incoming request object: this means the login attempt was ne and we can generate a
random string and persist the value as a token. We also assign the user id to the newly
created token and nally return with a User.Token.Detail response.

The initial user authentication will happen through a UserCredentialsAuthenticator, which


we've created back in Chapter 5. If the user sends a valid email and password through the
JSON request, this middleware will store the authenticated user details in the request.
Here's how to enable the credentials authenticator for the sign-in API route.

/// FILE: Sources/App/Modules/User/UserRouter.swift

import Vapor

struct UserRouter: RouteCollection {

let frontendController = UserFrontendController()


let apiController = UserApiController()

func boot(routes: RoutesBuilder) throws {


routes.get("sign-in", use: frontendController.signInView)
routes
.grouped(UserCredentialsAuthenticator())

234
fi
fi
.post("sign-in", use: frontendController.signInAction)

routes.get("sign-out", use: frontendController.signOut)

routes.grouped("api")
.grouped(UserCredentialsAuthenticator())
.post("sign-in", use: apiController.signInApi)
}
}

That's how we can create a very simple token-based authentication system using Vapor's
components. Feel free to run the server and try out the new sign-in endpoint using cURL.

curl -i \
-X POST "http://localhost:8080/api/sign-in/" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
--data-binary @- << EOF
{
"email": "[email protected]",
"password": "ChangeMe1"
}
EOF

You should get back a token object with an identi er and a randomly generated strange-
looking token value. You'll be able to use the value just a few minutes later to send a new
authenticated request. We're going to use it as a bearer authorization header value.

PROTECTING API ENDPOINTS


Now that we've got the API login mechanics, we can focus on a new
UserTokenAuthenticator struct, which is going to be responsible for authenticating a user
account via an incoming HTTP Authorization header with a Bearer token value.

The AsyncBearerAuthenticator protocol can fetch a Bearer token value from the proper
header, this way we can use the bearer value to look for an existing user token inside the
database. If we nd a correct token we can look up the corresponding user via the user
identi er stored next to the token value. If there's an existing user, we can authenticate it.

/// FILE: Sources/App/Modules/User/Authenticators/UserTokenAuthenticator.swift

import Vapor
import Fluent

struct UserTokenAuthenticator: AsyncBearerAuthenticator {

func authenticate(
bearer: BearerAuthorization,
for req: Request
) async throws {
guard
let token = try await UserTokenModel
.query(on: req.db)
.filter(\.$value == bearer.token)
.first()
else {
return
}

235
fi
fi
fi
guard
let user = try await UserAccountModel
.find(token.$user.id, on: req.db)
else {
return
}
req.auth.login(
AuthenticatedUser(
id: user.id!,
email: user.email
)
)
}
}

Of course, we could add expiration dates and more complicated security stu to this
authenticator, but for now, we're going to be ne. Now let me show you how to return a new
token object after a successful sign-in request. We'll create a UserApiController for this
purpose.

A very basic solution to authenticate users is to simply add the token authenticator as a
middleware to the entire system. In a production environment, we should only enable the
session authenticator for web-based routes and the token authenticator for API endpoints.

/// FILE: Sources/App/Modules/User/UserModule.swift


import Vapor

struct UserModule: ModuleInterface {

let router = UserRouter()

func boot(_ app: Application) throws {


app.migrations.add(UserMigrations.v1())
app.migrations.add(UserMigrations.seed())

app.middleware.use(UserSessionAuthenticator())
app.middleware.use(UserTokenAuthenticator())

try router.boot(routes: app.routes)


}
}

That's just one part of the API protection, we still need a way to guard the endpoints against
unauthenticated requests. Fortunately, we can use the guardMiddleware() method on the
AuthenticatedUser object, this will throw a proper error if there's no user signed in.

/// FILE: Sources/App/Modules/Blog/BlogRouter.swift

import Vapor

struct BlogRouter: RouteCollection {

let frontendController = BlogFrontendController()

let postAdminController = BlogPostAdminController()


let postApiController = BlogPostApiController()

let categoryAdminController = BlogCategoryAdminController()


let categoryApiController = BlogCategoryApiController()

func boot(routes: RoutesBuilder) throws {


routes.get("blog", use: frontendController.blogView)
routes.get(.anything, use: frontendController.postView)

let admin = routes


.grouped(AuthenticatedUser.redirectMiddleware(path: "/"))

236
fi
ff
.grouped("admin")

postAdminController.setupRoutes(admin)
categoryAdminController.setupRoutes(admin)

let api = routes


.grouped(AuthenticatedUser.guardMiddleware())
.grouped("api")
postApiController.setupRoutes(api)
categoryApiController.setupRoutes(api)
}
}

If you try to create a new category as a guest you should get an unauthorized response.

curl -i -X POST "http://localhost:8080/api/blog/categories/" \


-H "Accept: application/json" \
-H "Content-Type: application/json" \
--data-binary @- << EOF
{
"title": "Lorem ipsum"
}
EOF

# HTTP/1.1 401 Unauthorized


# AuthenticatedUser not authenticated."

Now try to use the token value from the previous sign-in response (cURL) as a bearer token.
You should be able to create a new blog category, but the API is still far from perfect.

API VALIDATION
If you try to submit an authenticated request to create a new blog category object, but you
mess up the post body and you don't provide a valid title value, strange things are going to
happen.

curl -i \
-X POST "http://localhost:8080/api/blog/categories/" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer [token]" \
--data-binary @- << EOF
{
"name": "Lorem ipsum"
}
EOF

# HTTP/1.1 500 Internal Server Error


# The data couldn’t be read because it is missing.

As you can see there was an internal server error because we've tried to create a new blog
category input object without a valid title value. Fortunately, the type-safe input decoding
mechanism protected us from further issues, there was no invalid data persisted in the
database, but still, this solution is bad and we should perform some sort of input validation.

237
We're going to reuse the AsyncValidator protocol for this purpose. In Chapter 7 we've
learned how to build a FormFieldValidator to check form elds, now we can apply that
technique to make sure the input data sent through the API is also correct. Let's build a
KeyedContentValidator rst.

/// FILE: Sources/App/Framework/Validation/KeyedContentValidator.swift

import Vapor

public struct KeyedContentValidator<T: Codable>: AsyncValidator {

public let key: String


public let message: String
public let optional: Bool
public let validation: (T, Request) async throws -> Bool

public init(
_ key: String,
_ message: String,
optional: Bool = false,
_ validation: @escaping (T, Request) async throws -> Bool
) {
self.key = key
self.message = message
self.optional = optional
self.validation = validation
}

public func validate(


_ req: Request
) async throws -> ValidationErrorDetail? {
let optionalValue = try? req.content.get(T.self, at: key)
if let value = optionalValue {
return try await validation(value, req) ? nil : error
}
return optional ? nil : error
}
}

This struct is very similar to a form eld validator, but it's generic, so we can decode the type
based on the Codable T type. We also pass a key to look up our T value, an error message,
an optional ag so we can only check the eld if it's required, plus a validation block where
we can perform the actual validation logic when it's needed.

Here are some sample validators that you can de ne as an extension on the
KeyedContentValidator. As you can see it's quite trivial to implement various type-safe
validators.

/// FILE: Sources/App/Framework/Validation/KeyedContentValidator+Validations.swift


import Vapor

public extension KeyedContentValidator where T == String {

static func required(


_ key: String,
_ message: String? = nil,
optional: Bool = false
) -> KeyedContentValidator<T> {
let msg = message ?? "\(key.capitalized) is required"
return .init(key, msg, optional: optional) { value, _ in
!value.isEmpty
}
}

static func min(


_ key: String,

238
fl
fi
fi
fi
fi
fi
_ length: Int,
_ message: String? = nil,
optional: Bool = false
) -> KeyedContentValidator<T> {
let msg = message ?? "\(key.capitalized) is too short (min: \(length)
characters)"
return .init(key, msg, optional: optional) { value, _ in
value.count >= length
}
}

static func max(


_ key: String,
_ length: Int,
_ message: String? = nil,
optional: Bool = false
) -> KeyedContentValidator<T> {
let msg = message ?? "\(key.capitalized) is too long (max: \(length)
characters)"
return .init(key, msg, optional: optional) { value, _ in
value.count <= length
}
}

static func alphanumeric(


_ key: String,
_ message: String? = nil,
_ optional: Bool = false
) -> KeyedContentValidator<T> {
let msg = message ?? "\(key.capitalized) should be only alphanumeric
characters"
return .init(key, msg, optional: optional) { value, _ in
!Validator.characterSet(.alphanumerics).validate(value).isFailure
}
}

static func email(


_ key: String,
_ message: String? = nil,
_ optional: Bool = false
) -> KeyedContentValidator<T> {
let msg = message ?? "\(key.capitalized) should be a valid email address"
return .init(key, msg, optional: optional) { value, _ in
!Validator.email.validate(value).isFailure
}
}
}

public extension KeyedContentValidator where T == Int {

static func min(


_ key: String,
_ length: Int,
_ message: String? = nil,
optional: Bool = false
) -> KeyedContentValidator<T> {
let msg = message ?? "\(key.capitalized) is too short (min: \(length)
characters)"
return .init(key, msg, optional: optional) { value, _ in
value >= length
}
}

static func max(


_ key: String,
_ length: Int,
_ message: String? = nil,
optional: Bool = false
) -> KeyedContentValidator<T> {
let msg = message ?? "\(key.capitalized) is too long (max: \(length)
characters)"
return .init(key, msg, optional: optional) { value, _ in
value <= length
}

239
}

static func contains(


_ key: String,
_ values: [Int],
_ message: String? = nil,
optional: Bool = false
) -> KeyedContentValidator<T> {
let msg = message ?? "\(key.capitalized) is an invalid value"
return .init(key, msg, optional: optional) { value, _ in
values.contains(value)
}
}
}

public extension KeyedContentValidator where T == UUID {

static func required(


_ key: String,
_ message: String? = nil,
optional: Bool = false
) -> KeyedContentValidator<T> {
let msg = message ?? "\(key.capitalized) is required"
return .init(key, msg, optional: optional) { _, _ in
true
}
}
}

The next step is to alter the ApiCreateController and somehow return a bunch of
AsyncValidator objects, so we can build a new RequestValidator and call the validate
method on that.

The request validator will throw an error this time because we don't have to set back the
invalid elds on a form, but we can directly return with an Abort error if something goes
wrong.

/// FILE: Sources/App/Modules/Api/Controllers/ApiCreateController.swift

import Vapor

protocol ApiCreateController: CreateController {


associatedtype CreateObject: Decodable

func createValidators() -> [AsyncValidator]

func createInput(
_ req: Request,
_ model: DatabaseModel,
_ input: CreateObject
) async throws

func createApi(
_ req: Request
) async throws -> Response

func createResponse(
_ req: Request,
_ model: DatabaseModel
) async throws -> Response

func setupCreateRoutes(
_ routes: RoutesBuilder
)
}

extension ApiCreateController {

func createValidators() -> [AsyncValidator] {

240
fi
[]
}

func createApi(
_ req: Request
) async throws -> Response {
try await RequestValidator(createValidators()).validate(req)
let input = try req.content.decode(CreateObject.self)
let model = DatabaseModel()
try await createInput(req, model, input)
try await model.create(on: req.db)
return try await createResponse(req, model)
}

func setupCreateRoutes(
_ routes: RoutesBuilder
) {
let baseRoutes = getBaseRoutes(routes)
baseRoutes.post(use: createApi)
}
}

Inside the BlogCategoryApiController you can implement a createValidators function and


return the necessary validator instances to check the input data.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogCategoryController.swift

import Vapor

extension Blog.Category.List: Content {}


extension Blog.Category.Detail: Content {}

struct BlogCategoryApiController: ApiController {


typealias ApiModel = Blog.Category
typealias DatabaseModel = BlogCategoryModel

@AsyncValidatorBuilder
func createValidators() -> [AsyncValidator] {
KeyedContentValidator<String>.required("title")
}

func listOutput(
_ req: Request,
_ models: [BlogCategoryModel]
) async throws -> [Blog.Category.List] {
models.map {
.init(id: $0.id!, title: $0.title)
}
}

func detailOutput(
_ req: Request,
_ model: BlogCategoryModel
) async throws -> Blog.Category.Detail {
.init(
id: model.id!,
title: model.title
)
}

func createInput(
_ req: Request,
_ model: BlogCategoryModel,
_ input: Blog.Category.Create
) async throws {
model.title = input.title
}

func updateInput(
_ req: Request,
_ model: BlogCategoryModel,

241
_ input: Blog.Category.Update
) async throws {
model.title = input.title
}

func patchInput(
_ req: Request,
_ model: BlogCategoryModel,
_ input: Blog.Category.Patch
) async throws {
model.title = input.title ?? model.title
}
}

For now, we only check if there was a title or not, let's execute a new cURL request real
quick.

curl -i \
-X POST "http://localhost:8080/api/blog/categories/" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer [token]" \
--data-binary @- << EOF
{
"name": "test category"
}
EOF

# HTTP/1.1 400 Bad Request


# {"error":true,"reason":"Bad Request"}

As you can see, the response is now a Bad Request error, which is good, but wouldn't be
better if we could see the underlying error details as well?

Let's create a new ValidationError struct, we're going to place all the ValidationErrorDetail
objects into a details array. Since the RequestValidator throws a custom ValidationAbort if
something goes wrong, we'll be able to get the error details from that object.

/// FILE: Sources/App/Framework/Validation/ValidationError.swift

import Vapor

struct ValidationError: Codable {

let message: String?


let details: [ValidationErrorDetail]

init(
message: String?,
details: [ValidationErrorDetail]
) {
self.message = message
self.details = details
}
}

extension ValidationError: Content {}

We'll need a new ApiErrorMiddleware to handle these cases. We simply try to respond
using the next responder and if there was an error we examine the message in the catch
block.

242
We're going to need a few variables to respond with a custom response. Of course, we're
going to need an HTTP status code, the response headers, a custom message, and the
validation error details.

In the switch block, we can check if the error was a custom ValidationAbort; if that's the
case we can simply set all the necessary values using that struct.

If the error was a regular Abort, we set the status, the headers, and the reason, but set the
details to an empty array, since there were no associated error details for Vapor's built-in
abort type.

The default response is going to be an internal server error; since we don't know much
about the underlying error, we can erase the headers and the detail elds, and we can check
if the current environment is debugged, so we can only show the error message for
developers.

We also log the error using Vapor's logging system and we respond with the Response using
an encoded JSON string or a plain text output.

/// FILE: Sources/App/Modules/Api/Middlewares/ApiErrorMiddleware.swift

import Vapor

struct ApiErrorMiddleware: AsyncMiddleware {

func respond(
to req: Request,
chainingTo next: AsyncResponder
) async throws -> Response {
do {
return try await next.respond(to: req)
}
catch {
let status: HTTPResponseStatus
let headers: HTTPHeaders
let message: String?
let details: [ValidationErrorDetail]

switch error {
case let abort as ValidationAbort:
status = abort.abort.status
headers = abort.abort.headers
message = abort.message
details = abort.details
case let abort as Abort:
status = abort.status
headers = abort.headers
message = abort.reason
details = []
default:
status = .internalServerError
headers = [:]
if req.application.environment.isRelease {
message = "Something went wrong."
}
else {
message = error.localizedDescription
}
details = []
}

req.logger.report(error: error)

let response = Response(

243
fi
status: status,
headers: headers
)

do {
let encoder = JSONEncoder()
let data = try encoder.encode(
ValidationError(
message: message,
details: details
)
)
response.body = .init(data: data)
response.headers.replaceOrAdd(
name: .contentType,
value: "application/json; charset=utf-8"
)
}
catch {
response.body = .init(string: "Oops: \(error)")
response.headers.replaceOrAdd(
name: .contentType,
value: "text/plain; charset=utf-8"
)
}
return response
}
}
}

We just have to create a new ApiModule and register this newly created middleware in the
boot function.

/// FILE: Sources/App/Modules/Api/ApiModule.swift

import Vapor

struct ApiModule: ModuleInterface {

func boot(_ app: Application) throws {


app.middleware.use(ApiErrorMiddleware())
}
}

Don't forget to add the module to the con guration le.

/// FILE: Sources/App/con gure.swift

import Vapor
import Fluent
import FluentSQLiteDriver
import Liquid
import LiquidLocalDriver

public func configure(


_ app: Application
) throws {

app.fileStorages.use(
.local(
publicUrl: "http://localhost:8080",
publicPath: app.directory.publicDirectory,
workDirectory: "assets"
),
as: .local
)

app.routes.defaultMaxBodySize = "10mb"

244
fi
fi
fi
let dbPath = app.directory.resourcesDirectory + "db.sqlite"
app.databases.use(.sqlite(.file(dbPath)), as: .sqlite)

app.middleware.use(
FileMiddleware(
publicDirectory: app.directory.publicDirectory
)
)

app.middleware.use(ExtendPathMiddleware())

app.sessions.use(.fluent)
app.migrations.add(SessionRecord.migration)
app.middleware.use(app.sessions.middleware)

let modules: [ModuleInterface] = [


WebModule(),
UserModule(),
AdminModule(),
ApiModule(),
BlogModule(),
]
for module in modules {
try module.boot(app)
}

try app.autoMigrate().wait()
}

Now if you run the same invalid request you should see a bit more detailed error info.

curl -i \
-X POST "http://localhost:8080/api/blog/categories/" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer [token]" \
--data-binary @- << EOF
{
"name": "test category"
}
EOF

# HTTP/1.1 400 Bad Request

# {"details":[{"key":"title","message":"Title is required"}]}%

It's much better because clients can now decode the error response and display the proper
error message next to the native input eld if necessary. Of course, if there were multiple
issues the server will return the most recent problem for each eld.

Let's move forward with the update and patch validators. We can simply introduce a new
method for these controllers, so rst let's add the following.

/// FILE: Sources/App/Modules/Api/Controllers/ApiUpdateController.swift

import Vapor

public protocol ApiUpdateController: UpdateController {


associatedtype UpdateObject: Decodable

func updateValidators() -> [AsyncValidator]

func updateInput(
_ req: Request,
_ model: DatabaseModel,
_ input: UpdateObject
) async throws

245
fi
fi
fi
func updateApi(
_ req: Request
) async throws -> Response

func updateResponse(
_ req: Request,
_ model: DatabaseModel
) async throws -> Response

func setupUpdateRoutes(
_ routes: RoutesBuilder
)
}

public extension ApiUpdateController {

func updateValidators() -> [AsyncValidator] {


[]
}

func updateApi(
_ req: Request
) async throws -> Response {
try await RequestValidator(updateValidators()).validate(req)
let model = try await findBy(identifier(req), on: req.db)
let input = try req.content.decode(UpdateObject.self)
try await updateInput(req, model, input)
try await model.update(on: req.db)
return try await updateResponse(req, model)
}

func setupUpdateRoutes(
_ routes: RoutesBuilder
) {
let baseRoutes = getBaseRoutes(routes)
let existingModelRoutes = baseRoutes
.grouped(ApiModel.pathIdComponent)

existingModelRoutes.put(use: updateApi)
}
}

Next we can insert a similar patchValidators function to the ApiPatchController.

/// FILE: Sources/App/Modules/Api/Controllers/ApiPatchController.swift

import Vapor

public protocol ApiPatchController: PatchController {


associatedtype PatchObject: Decodable

func patchValidators() -> [AsyncValidator]

func patchInput(
_ req: Request,
_ model: DatabaseModel,
_ input: PatchObject
) async throws

func patchApi(
_ req: Request
) async throws -> Response

func patchResponse(
_ req: Request,
_ model: DatabaseModel
) async throws -> Response

func setupPatchRoutes(
_ routes: RoutesBuilder
)

246
}

public extension ApiPatchController {

func patchValidators() -> [AsyncValidator] {


[]
}

func patchApi(
_ req: Request
) async throws -> Response {
try await RequestValidator(patchValidators()).validate(req)
let model = try await findBy(identifier(req), on: req.db)
let input = try req.content.decode(PatchObject.self)
try await patchInput(req, model, input)
try await model.update(on: req.db)
return try await patchResponse(req, model)
}

func setupPatchRoutes(
_ routes: RoutesBuilder
) {
let baseRoutes = getBaseRoutes(routes)
let existingModelRoutes = baseRoutes
.grouped(ApiModel.pathIdComponent)

existingModelRoutes.patch(use: patchApi)
}
}

These methods would work just ne, but since developers don't like code repetition and we
already have an ApiController instance we can easily create a new helper method to return
our validators with an optional ag and use the same validators for all three endpoints.

/// FILE: Sources/App/Modules/Api/Controllers/ApiController.swift

import Vapor

protocol ApiController:
ApiListController,
ApiDetailController,
ApiCreateController,
ApiUpdateController,
ApiPatchController,
ApiDeleteController
{
func validators(
optional: Bool
) -> [AsyncValidator]

func setupRoutes(
_ routes: RoutesBuilder
)
}

extension ApiController {

func validators(
optional: Bool
) -> [AsyncValidator] {
[]
}

func createValidators() -> [AsyncValidator] {


validators(optional: false)
}

func updateValidators() -> [AsyncValidator] {


validators(optional: false)
}

247
fl
fi
func patchValidators() -> [AsyncValidator] {
validators(optional: true)
}

func createResponse(
_ req: Request,
_ model: DatabaseModel
) async throws -> Response {
try await detailOutput(req, model)
.encodeResponse(status: .created, for: req)
}

func updateResponse(
_ req: Request,
_ model: DatabaseModel
) async throws -> Response {
try await detailOutput(req, model)
.encodeResponse(for: req)
}

func patchResponse(
_ req: Request,
_ model: DatabaseModel
) async throws -> Response {
try await detailOutput(req, model)
.encodeResponse(for: req)
}

func setupRoutes(
_ routes: RoutesBuilder
) {
setupListRoutes(routes)
setupDetailRoutes(routes)
setupCreateRoutes(routes)
setupUpdateRoutes(routes)
setupPatchRoutes(routes)
setupDeleteRoutes(routes)
}
}

This is how you can return the validators inside the BlogCategoryApiController to validate
the create, update and patch input values at once.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogCategoryApiController.swift

import Vapor

extension Blog.Category.List: Content {}


extension Blog.Category.Detail: Content {}

struct BlogCategoryApiController: ApiController {


typealias ApiModel = Blog.Category
typealias DatabaseModel = BlogCategoryModel

@AsyncValidatorBuilder
func validators(
optional: Bool
) -> [AsyncValidator] {
KeyedContentValidator<String>.required("title", optional: optional)
}

func listOutput(
_ req: Request,
_ models: [BlogCategoryModel]
) async throws -> [Blog.Category.List] {
models.map {
.init(id: $0.id!, title: $0.title)
}
}

func detailOutput(

248
_ req: Request,
_ model: BlogCategoryModel
) async throws -> Blog.Category.Detail {
.init(
id: model.id!,
title: model.title
)
}

func createInput(
_ req: Request,
_ model: BlogCategoryModel,
_ input: Blog.Category.Create
) async throws {
model.title = input.title
}

func updateInput(
_ req: Request,
_ model: BlogCategoryModel,
_ input: Blog.Category.Update
) async throws {
model.title = input.title
}

func patchInput(
_ req: Request,
_ model: BlogCategoryModel,
_ input: Blog.Category.Patch
) async throws {
model.title = input.title ?? model.title
}
}

A more complex example is the blog post controller since blog posts have more elds. You
can also come up with a custom KeyedContentValidator on the y since the last parameter
is a validation block that you can use to check the input value and the incoming request.

We've only barely touched the BlogPostApiController, so now I'm going to implement a
somewhat proper input handler for the CRUD, there are going to be some mistakes, but in
the next chapter, we're going to catch those through proper unit testing.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogPostApiController.swift

import Vapor

extension Blog.Post.List: Content {}


extension Blog.Post.Detail: Content {}

struct BlogPostApiController: ApiController {


typealias ApiModel = Blog.Post
typealias DatabaseModel = BlogPostModel

@AsyncValidatorBuilder
func validators(optional: Bool) -> [AsyncValidator] {
KeyedContentValidator<String>.required("title", optional: optional)
KeyedContentValidator<String>.required("slug", optional: optional)
KeyedContentValidator<String>.required("image", optional: optional)
KeyedContentValidator<String>.required("excerpt", optional: optional)
KeyedContentValidator<String>.required("content", optional: optional)
KeyedContentValidator<UUID>.required("categoryId", optional: optional)
KeyedContentValidator<UUID>("categoryId", "Invalid or missing category",
optional: optional) { value, req in
try await BlogCategoryModel.find(value, on: req.db) != nil
}
}

func listOutput(
_ req: Request,

249
fl
fi
_ models: [DatabaseModel]
) async throws -> [Blog.Post.List] {
models.map { model in
.init(
id: model.id!,
title: model.title,
slug: model.slug,
image: model.imageKey,
excerpt: model.excerpt,
date: model.date
)
}
}

func detailOutput(
_ req: Request,
_ model: DatabaseModel
) async throws -> Blog.Post.Detail {
.init(
id: model.id!,
title: model.title,
slug: model.slug,
image: model.imageKey,
excerpt: model.excerpt,
date: model.date,
category: .init(
id: model.category.id!,
title: model.category.title
),
content: model.content
)
}

func createInput(
_ req: Request,
_ model: DatabaseModel,
_ input: Blog.Post.Create
) async throws {
model.title = input.title
model.slug = input.slug
model.imageKey = input.image
model.excerpt = input.excerpt
model.date = input.date
model.content = input.content
}

func updateInput(
_ req: Request,
_ model: DatabaseModel,
_ input: Blog.Post.Update
) async throws {
model.title = input.title
model.slug = input.slug
model.imageKey = input.image
model.excerpt = input.excerpt
model.date = input.date
model.content = input.content
}

func patchInput(
_ req: Request,
_ model: DatabaseModel,
_ input: Blog.Post.Patch
) async throws {
model.title = input.title ?? model.title
model.slug = input.slug ?? model.slug
model.imageKey = input.image ?? model.imageKey
model.excerpt = input.excerpt ?? model.excerpt
model.date = input.date ?? model.date
model.content = input.content ?? model.content
}
}

250
You can try out the update or patch methods as well, you just need a valid post identi er.

curl -i \
-X POST "http://localhost:8080/api/blog/posts/[uuid]/" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer [token]" \
--data-binary @- << EOF
{
"title": "test category"
}
EOF

That's how you can validate incoming data, it's very helpful that we can use the same async
validation logic for both the admin and the API controllers. Unfortunately, Vapor's built-in
validation APIs aren't so capable, but you can use those inside your validation blocks.

API LIFECYCLE METHODS


There's one more thing that we should address in this chapter. Blog posts can have
associated image objects, but if you delete a blog post those les are still going to exist
inside the le storage. How can we delete them after a delete action happens?

The general idea is to create lifecycle methods to perform additional actions before and
after the main controller action. These are the events that we can easily create.

- before / after list

- before / after detail

- before / after create

- before / after update

- before / after patch

- before / after delete

This is the reason why we created one more abstraction layer for the controllers on top of
the admin and API controllers. Now we can extend the top level: this way the protocols on
the bottom level will also bene t from the lifecycle events.

As always, we can start with the ListController, this time it's going to be a bit special
because the beforeList method will have a QueryBuilder parameter, which can be used to
alter the list query that's going to be executed. In the afterList method developers will have
a chance to alter the list returned by the database query.

/// FILE: Sources/App/Framework/Controllers/ListController.swift

import Vapor
import Fluent

protocol ListController: ModelController {

251
fi
fi
fi
fi
func list(
_ req: Request
) async throws -> [DatabaseModel]

func beforeList(
_ req: Request,
_ queryBuilder: QueryBuilder<DatabaseModel>
) async throws -> QueryBuilder<DatabaseModel>

func afterList(
_ req: Request,
_ models: [DatabaseModel]
) async throws -> [DatabaseModel]
}

extension ListController {

func list(
_ req: Request
) async throws -> [DatabaseModel] {
try await DatabaseModel
.query(on: req.db)
.all()
}

func beforeList(
_ req: Request,
_ queryBuilder: QueryBuilder<DatabaseModel>
) async throws -> QueryBuilder<DatabaseModel> {
queryBuilder
}

func afterList(
_ req: Request,
_ models: [DatabaseModel]
) async throws -> [DatabaseModel] {
models
}
}

We also have to change the DetailController quite a bit. Previously we've used the ndBy
method to look up our detail object, but now it's time to introduce a standalone detail
function plus the lifecycle methods. They'll look quite similar to the list functions, but this
time they'll only have a single model parameter instead of an array of models.

/// FILE: Sources/App/Framework/Controllers/DetailController.swift

import Vapor
import Fluent

protocol DetailController: ModelController {

func detail(
_ req: Request
) async throws -> DatabaseModel

func beforeDetail(
_ req: Request,
_ queryBuilder: QueryBuilder<DatabaseModel>
) async throws -> QueryBuilder<DatabaseModel>

func afterDetail(
_ req: Request,
_ model: DatabaseModel
) async throws -> DatabaseModel

extension DetailController {

252
fi
func detail(
_ req: Request
) async throws -> DatabaseModel {

let queryBuilder = DatabaseModel.query(on: req.db)


let model = try await beforeDetail(req, queryBuilder)
.filter(\._$id == identifier(req))
.first()

guard let model = model else {


throw Abort(.notFound)
}
return try await afterDetail(req, model)
}

func beforeDetail(
_ req: Request,
_ queryBuilder: QueryBuilder<DatabaseModel>
) async throws -> QueryBuilder<DatabaseModel> {
queryBuilder
}

func afterDetail(
_ req: Request,
_ model: DatabaseModel
) async throws -> DatabaseModel {
model
}
}

We have to update the AdminDetailController and call the detail method.

/// FILE: Sources/App/Modules/Admin/Controllers/AdminDetailController.swift

import Vapor

protocol AdminDetailController: DetailController {


// ...
}

extension AdminDetailController {

func detailView(
_ req: Request
) async throws -> Response {
let model = try await detail(req)
return req.templates.renderHtml(detailTemplate(req, model))
}

// ...

Inside the ApiDetailController we also have to swap out the ndBy method with the detail.

/// FILE: Sources/App/Modules/Api/Controllers/ApiDetailController.swift

import Vapor

protocol ApiDetailController: DetailController {


// ...
}

extension ApiDetailController {

func detailApi(
_ req: Request
) async throws -> DetailObject {
let model = try await detail(req)

253
fi
return try await detailOutput(req, model)
}

// ...
}

Now it's time to focus on the CreateController, we're going to introduce a new create
method and call the before and after lifecycle functions just like we did for the other
controllers.

/// FILE: Sources/App/Framework/Controllers/CreateController.swift

import Vapor

protocol CreateController: ModelController {

func create(
_ req: Request,
_ model: DatabaseModel
) async throws

func beforeCreate(
_ req: Request,
_ model: DatabaseModel
) async throws

func afterCreate(
_ req: Request,
_ model: DatabaseModel
) async throws
}

extension CreateController {

func create(
_ req: Request,
_ model: DatabaseModel
) async throws {
try await beforeCreate(req, model)
try await model.create(on: req.db)
try await afterCreate(req, model)
}

func beforeCreate(
_ req: Request,
_ model: DatabaseModel
) async throws {}

func afterCreate(
_ req: Request,
_ model: DatabaseModel
) async throws {}
}

We only have to change one line inside the AdminCreateController.

/// FILE: Sources/App/Modules/Admin/Controllers/AdminCreateController.swift

import Vapor

protocol AdminCreateController: CreateController {


// ...
}

extension AdminCreateController {

// ...

func createAction(

254
_ req: Request
) async throws -> Response {
let model = DatabaseModel()
let editor = CreateModelEditor(
model: model as! CreateModelEditor.Model,
form: .init()
)
editor.form.fields = editor.formFields
try await editor.load(req: req)
try await editor.process(req: req)
let isValid = try await editor.validate(req: req)
guard isValid else {
return render(req, editor: editor)
}
try await editor.write(req: req)
try await create(req, editor.model as! DatabaseModel)
try await editor.save(req: req)
var components = req.url.path.pathComponents.dropLast()
components += editor.model.id!.uuidString.pathComponents
return req.redirect(to: "/" + components.string + "/update/")

}
// ...
}

The same thing applies to the ApiCreateController; just call the create function: this will
trigger the proper lifecycle events as well in both cases.

/// FILE: Sources/App/Modules/Api/Controllers/ApiCreateController.swift

import Vapor

protocol ApiCreateController: CreateController {


// ...
}

extension ApiCreateController {

// ...

func createApi(
_ req: Request
) async throws -> Response {
try await RequestValidator(createValidators()).validate(req)
let input = try req.content.decode(CreateObject.self)
let model = DatabaseModel()
try await createInput(req, model, input)
try await create(req, model)
return try await createResponse(req, model)
}

// ...
}

Inside the UpdateController we can have the same methods: of course, we've changed the
pre x.

/// FILE: Sources/App/Framework/Controllers/UpdateController.swift

import Vapor

public protocol UpdateController: ModelController {

func update(
_ req: Request,
_ model: DatabaseModel
) async throws

func beforeUpdate(

255
fi
_ req: Request,
_ model: DatabaseModel
) async throws

func afterUpdate(
_ req: Request,
_ model: DatabaseModel
) async throws

public extension UpdateController {

func update(
_ req: Request,
_ model: DatabaseModel
) async throws {
try await beforeUpdate(req, model)
try await model.update(on: req.db)
try await afterUpdate(req, model)
}
func beforeUpdate(
_ req: Request,
_ model: DatabaseModel
) async throws {}

func afterUpdate(
_ req: Request,
_ model: DatabaseModel
) async throws {}
}

Change the AdminUpdateController and call the async update method using the model
from the editor.

/// FILE: Sources/App/Modules/Admin/Controllers/AdminUpdateController.swift

import Vapor

protocol AdminUpdateController: UpdateController {


// ...
}

extension AdminUpdateController {

// ...

func updateAction(
_ req: Request
) async throws -> Response {
let model = try await findBy(identifier(req), on: req.db)
let editor = UpdateModelEditor(
model: model as! UpdateModelEditor.Model,
form: .init()
)
editor.form.fields = editor.formFields
try await editor.load(req: req)
try await editor.process(req: req)
let isValid = try await editor.validate(req: req)
guard isValid else {
return await render(req, editor: editor)
}
try await editor.write(req: req)
try await update(req, editor.model as! DatabaseModel)
try await editor.save(req: req)
return req.redirect(to: req.url.path)
}

// ...
}

256
In the ApiUpdateController we should also call the update method.

/// FILE: Sources/App/Modules/Api/Controllers/ApiUpdateController.swift

import Vapor

protocol ApiUpdateController: UpdateController {


// ...
}

extension ApiUpdateController {

// ...

func updateApi(
_ req: Request
) async throws -> Response {
try await RequestValidator(updateValidators()).validate(req)
let model = try await findBy(identifier(req), on: req.db)
let input = try req.content.decode(UpdateObject.self)
try await updateInput(req, model, input)
try await update(req, model)
return try await updateResponse(req, model)
}

// ...
}

The PatchController will follow the same pattern: I guess you know what happens next.

/// FILE: Sources/App/Framework/Controllers/PatchController.swift

import Vapor

public protocol PatchController: ModelController {

func patch(
_ req: Request,
_ model: DatabaseModel
) async throws

func beforePatch(
_ req: Request,
_ model: DatabaseModel
) async throws

func afterPatch(
_ req: Request,
_ model: DatabaseModel
) async throws

public extension PatchController {

func patch(
_ req: Request,
_ model: DatabaseModel
) async throws {
try await beforePatch(req, model)
try await model.update(on: req.db)
try await afterPatch(req, model)
}

func beforePatch(
_ req: Request,
_ model: DatabaseModel
) async throws {}

func afterPatch(
_ req: Request,

257
_ model: DatabaseModel
) async throws {}
}

As it happens there's no admin patch controller, so we only have to update one single le.

/// FILE: Sources/App/Modules/Api/Controllers/ApiPatchController.swift

import Vapor

protocol ApiPatchController: PatchController {


// ...
}

extension ApiPatchController {

// ...

func patchApi(
_ req: Request
) async throws -> Response {
try await RequestValidator(patchValidators()).validate(req)
let model = try await findBy(identifier(req), on: req.db)
let input = try req.content.decode(PatchObject.self)
try await patchInput(req, model, input)
try await patch(req, model)
return try await patchResponse(req, model)
}

// ...
}

The very last component is the DeleteController: we introduce the same lifecycle functions
here too.

/// FILE: Sources/App/Framework/Controllers/DeleteController.swift

import Vapor

public protocol DeleteController: ModelController {

func delete(
_ req: Request,
_ model: DatabaseModel
) async throws

func beforeDelete(
_ req: Request,
_ model: DatabaseModel
) async throws

func afterDelete(
_ req: Request,
_ model: DatabaseModel
) async throws
}

public extension DeleteController {

func delete(
_ req: Request,
_ model: DatabaseModel
) async throws {
try await beforeDelete(req, model)
try await model.delete(on: req.db)
try await afterDelete(req, model)
}

func beforeDelete(

258
fi
_ req: Request,
_ model: DatabaseModel
) async throws {}

func afterDelete(
_ req: Request,
_ model: DatabaseModel
) async throws {}
}

Inside the AdminDeleteController, we can call the delete method before the redirect.

/// FILE: Sources/App/Modules/Admin/Controllers/AdminDeleteController.swift

import Vapor

//...

protocol AdminDeleteController: DeleteController {

// ...
}

extension AdminDeleteController {

//...

func deleteAction(
_ req: Request
) async throws -> Response {
let model = try await findBy(identifier(req), on: req.db)
try await delete(req, model)

var url = req.url.path


if let redirect = try? req.query.get(String.self, at: "redirect") {
url = redirect
}
return req.redirect(to: url)

//...
}

As a very nal step, we can update the ApiDeleteController.

/// FILE: Sources/App/Modules/Api/Controllers/ApiDeleteController.swift

import Vapor

protocol ApiDeleteController: DeleteController {

// ...
}

extension ApiDeleteController {

func deleteApi(
_ req: Request
) async throws -> HTTPStatus {
let model = try await findBy(identifier(req), on: req.db)
try await delete(req, model)
return .noContent
}

// ...
}

259
fi
Now that we're ready with the controllers, you can try them out. For example, it's possible to
delete the image le before the actual post deletion inside the BlogPostAdminController.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogPostAdminController.swift

import Vapor
import Fluent

struct BlogPostAdminController: AdminController {

// ...

func beforeDelete(
_ req: Request,
_ model: BlogPostModel
) async throws {
try await req.fs.delete(key: model.imageKey)
}
}

This is how you can hook into these lifecycle methods; of course, you'll have to take care of
every single case, so if a deletion event happens through the API you'll have to remove the
associated image in that case as well.

SUMMARY
In this chapter we've learned how to protect our API endpoints from an unauthorized request
by using the user module and extending it with a token-based user authentication model.
Later on, we extended the controllers with a validation logic that'll allow us to validate input
data before we process and persist objects to the database. Finally, we've learned about
lifecycle methods; this way it's possible to hook into various controller events and perform
additional work if needed.

260
fi
CHAPTER 14:
SYSTEM UNDER TESTING

This chapter is about learning the brand new XCTVapor framework. First, we'll set up the test
environment, write some basic unit tests for our application, and run them. Next, we're going
to dig a little bit deeper into the XCTVapor framework so you can see how to write more
complex tests. In the last part, you'll meet with a super lightweight and clean testing tool. The
Spec library will allow us to write declarative speci cations for our test cases.

GETTING STARTED WITH UNIT TESTING


Let's start by creating a new folder we’ll use during this chapter. You can use the commands
below to add it:

cd ~/myProject
mkdir -p Tests/AppTests/Framework

To easily create test cases we'll have to add the XCTVapor framework to our test target as a
dependency if necessary. Since we created our project through the Vapor toolbox,
XCTVapor should already be part of the test target. You can also write unit tests without this
framework, by only relying on the XCTest framework, but I highly recommend using
XCTVapor too.

Before we can write our very rst test case, we should take a quick look at our current
project structure. The Test folder is part of the Swift package speci cation: it contains source
les for your test targets, and it works just like the Sources folder for regular targets.

Fortunately, we don't have to set the current working directory for test targets, since Xcode
will use the same option both for running and testing the application. You can long-press the
play button and select the test option, or use the Command + U combination to run all of the
tests, but my favorite keyboard shortcut is Control + Command + Option + G. This will re-run
your last test. From a terminal window, you can run all the tests by running the following
command.

swift test

You can list all the available tests via the -l or --list-tests ag. You can run the test in parallel
and even generate coverage data using the llvm-cov command. There's an experimental
tool on GitHub that can convert test result output to a JSON structure. You can use the lter
option to run test cases matching a regular expression.

# run all the tests in a serial order


swift test

# list available tests

261
fi
fi
fi
fl
fi
fi
swift test -l
swift test --list-tests

# run a specific test


swift test --filter AppTests.AppTests/testHelloWorld

# run all tests in parallel


swift test --parallel
swift test --parallel --num-workers 2

# install testify & display tests as json


git clone https://github.com/BinaryBirds/Testify.git
make install
swift test 2>&1 | testify | json

Now that we know how to run unit tests, it's time to create some non-failing test cases. The
most simple test that we can write is to check whether our home page works or not. We'll
make a new application instance for testing purposes only and send a new test request to
our server: we're going to validate if the response was an HTML document and contains the
"Home" title string.

/// FILE: Tests/AppTests/AppTests.swift

@testable import App


import XCTVapor

final class AppTests: XCTestCase {

func testHomePage() throws {


let app = Application(.testing)
defer { app.shutdown() }
try configure(app)

try app.testable(method: .inMemory).test(.GET, "") { res in


XCTAssertEqual(res.status, .ok)

let contentType = try XCTUnwrap(res.headers.contentType)


XCTAssertEqual(contentType, .html)
XCTAssertTrue(res.body.string.contains("Home"))
}
}
}

You should note that each test function should begin with the test pre x, the system will only
consider those methods as test cases. The XCTVapor framework gives us a really handy
test method that you can use to send requests to the application. You can decide if you'd
like to test everything in memory or via a running server instance.

In our case, we've performed a GET request using the root ("") endpoint; in the response
handler block we can check the headers: more precisely the contentType to validate that
the returned data is a .html type. As the last step, we check if the response HTML string
contains the necessary title value, so we can be sure that this is our generated home page.

We could parse the HTML structure and perform some additional checking, but that would
be an overkill for sure. Still, if you need to write more complex tests for validating HTML you
can use a third-party library like Kanna or something similar to parse HTML.

262
fi
TESTING WITH USER AUTHENTICATION
In this section, we're going to test some of our REST API endpoints. Since we don't want to
mess up our database we're going to override the database con guration with a new in-
memory SQLite database instance. This is going to be a brand new empty database, without
the tables, so we have to run the migrations after the database re-init process.

We can build a nice little helper function to create app instances for testing purposes.

/// FILE: Tests/AppTests/AppTests.swift

@testable import App


import XCTVapor

final class AppTests: XCTestCase {

private func createTestApp() throws -> Application {


let app = Application(.testing)

try configure(app)
app.databases.reinitialize()
app.databases.use(.sqlite(.memory), as: .sqlite)
app.databases.default(to: .sqlite)
try app.autoMigrate().wait()
return app
}

func testHomePage() throws {


let app = try createTestApp()
defer { app.shutdown() }

try app.testable(method: .inMemory).test(.GET, "") { res in


XCTAssertEqual(res.status, .ok)

let contentType = try XCTUnwrap(res.headers.contentType)


XCTAssertEqual(contentType, .html)
XCTAssertTrue(res.body.string.contains("Home"))
}
}
}

We can use this function to create a "System Under Test" (SUT) version of our app instance.
We don't have to worry about the original database anymore, the app will use the new in-
memory .sqlite connection by default for tests. Now we can start testing our API endpoints.

To send an API request we're going to need a bearer token. First, we have to create a login
function that we can use to submit the credentials of our seed user. We can exchange the
email password combination for a new API token. Here is how we can do that.

/// FILE: Tests/AppTests/AppTests.swift

@testable import App


import XCTVapor

final class AppTests: XCTestCase {

private func createTestApp() throws -> Application {


let app = Application(.testing)

try configure(app)
app.databases.reinitialize()
app.databases.use(.sqlite(.memory), as: .sqlite)

263
fi
app.databases.default(to: .sqlite)
try app.autoMigrate().wait()
return app
}

private struct UserLogin: Content {


let email: String
let password: String
}

private func authenticate(


_ user: UserLogin,
_ app: Application
) throws -> User.Token.Detail {
var token: User.Token.Detail?
try app.test(.POST, "/api/sign-in/", beforeRequest: { req in
try req.content.encode(user)
}, afterResponse: { res in
XCTAssertContent(User.Token.Detail.self, res) { content in
token = content
}
})
guard let result = token else {
XCTFail("Login failed")
throw Abort(.unauthorized)
}
return result
}

func testAuth() throws {


let app = try createTestApp()
defer { app.shutdown() }

let email = "[email protected]"


let token = try authenticate(
.init(
email: email,
password: "ChangeMe1"
),
app
)
XCTAssertEqual(token.user.email, email)
}

func testHomePage() throws {


let app = try createTestApp()
defer { app.shutdown() }

try app.testable(method: .inMemory).test(.GET, "") { res in


XCTAssertEqual(res.status, .ok)

let contentType = try XCTUnwrap(res.headers.contentType)


XCTAssertEqual(contentType, .html)
XCTAssertTrue(res.body.string.contains("Home"))
}
}
}

We have to encode the UserLogin object in the beforeRequest block, you can use the built-
in content encoder, which is available on the request itself. The test method will return the
raw response in the afterResponse block. We can use the XCTAssertContent helper to
decode the User.Token.Detail object from the result. We can store the token in a variable
outside of the whole test method.

The test function is using the wait method internally so you don't have to deal as much with
async blocks during tests. We can safely check for a token after the function has returned
and throw an error if we couldn't authorize it.

264
We also have to pass the application instance for the authenticate function since we don't
want to create a new app instance every time.

Now that we can get an auth token, we should try to call an existing API method.

/// FILE: Tests/AppTests/AppTests.swift

@testable import App


import XCTVapor

final class AppTests: XCTestCase {

private func createTestApp() throws -> Application {


let app = Application(.testing)

try configure(app)
app.databases.reinitialize()
app.databases.use(.sqlite(.memory), as: .sqlite)
app.databases.default(to: .sqlite)
try app.autoMigrate().wait()
return app
}

private struct UserLogin: Content {


let email: String
let password: String
}

private func authenticate(


_ user: UserLogin,
_ app: Application
) throws -> User.Token.Detail {
var token: User.Token.Detail?
try app.test(.POST, "/api/sign-in/", beforeRequest: { req in
try req.content.encode(user)
}, afterResponse: { res in
XCTAssertContent(User.Token.Detail.self, res) { content in
token = content
}
})
guard let result = token else {
XCTFail("Login failed")
throw Abort(.unauthorized)
}
return result
}

private func authenticateRoot(


_ app: Application
) throws -> User.Token.Detail {
try authenticate(
.init(
email: "[email protected]",
password: "ChangeMe1"
),
app
)
}

func testAuth() throws {


let app = try createTestApp()
defer { app.shutdown() }

let email = "[email protected]"


let token = try authenticate(
.init(
email: email,
password: "ChangeMe1"
),
app
)

265
XCTAssertEqual(token.user.email, email)
}

func testHomePage() throws {


let app = try createTestApp()
defer { app.shutdown() }

try app.testable(method: .inMemory).test(.GET, "") { res in


XCTAssertEqual(res.status, .ok)

let contentType = try XCTUnwrap(res.headers.contentType)


XCTAssertEqual(contentType, .html)
XCTAssertTrue(res.body.string.contains("Home"))
}
}

func testList() throws {


let app = try createTestApp()
let token = try authenticateRoot(app)
defer { app.shutdown() }

let headers = HTTPHeaders([("Authorization", "Bearer \(token.value)")])

try app
//.testable(method: .inMemory)
.testable(method: .running(port: 8081))
.test(.GET, "/api/blog/categories/", headers: headers) { res in
XCTAssertEqual(res.status, .ok)
let contentType = try XCTUnwrap(res.headers.contentType)
XCTAssertEqual(contentType, .json)
XCTAssertContent([Blog.Category.List].self, res) { content in
XCTAssertEqual(content.count, 4)
}
}
}
}

As I mentioned before, the nice thing about XCTVapor is that it'll allow you to test requests
programmatically or you can use a live HTTP server listening on a given port.
The .inMemory option is used by default, but you can override this behavior through
the .testable() method for each test case; let's try out the server variant this time.

In the example above we had to set the authorization header rst, and in the response, we
were validating the returned array of Blog.Category.List objects. We should expect a total
number of four elements because the seed data creates exactly 4 categories during the
migration process.

Now, there's one thing that you can do to make testing a little bit more pleasant experience.
We can extend the XCTApplicationTester protocol with a helper function so we don't have to
pass the beforeRequest block when we want to send a Content object as a request body.

To submit a Blog.Category.Create item, we have to conform it to the Content protocol. Let's


just place everything in the AppTests le for now.

/// FILE: Tests/AppTests/AppTests.swift

@testable import App


import XCTVapor

extension XCTApplicationTester {

@discardableResult public func test<T>(


_ method: HTTPMethod,
_ path: String,

266
fi
fi
headers: HTTPHeaders = [:],
content: T,
afterResponse: (XCTHTTPResponse) throws -> () = { _ in }
) throws -> XCTApplicationTester where T: Content {
try test(method, path, headers: headers, beforeRequest: { req in
try req.content.encode(content)
}, afterResponse: afterResponse)
}
}

extension Blog.Category.Create: Content {}

final class AppTests: XCTestCase {

private func createTestApp() throws -> Application {


let app = Application(.testing)

try configure(app)
app.databases.reinitialize()
app.databases.use(.sqlite(.memory), as: .sqlite)
app.databases.default(to: .sqlite)
try app.autoMigrate().wait()
return app
}

private struct UserLogin: Content {


let email: String
let password: String
}

private func authenticate(


_ user: UserLogin,
_ app: Application
) throws -> User.Token.Detail {
var token: User.Token.Detail?
try app.test(.POST, "/api/sign-in/", beforeRequest: { req in
try req.content.encode(user)
}, afterResponse: { res in
XCTAssertContent(User.Token.Detail.self, res) { content in
token = content
}
})
guard let result = token else {
XCTFail("Login failed")
throw Abort(.unauthorized)
}
return result
}

private func authenticateRoot(


_ app: Application
) throws -> User.Token.Detail {
try authenticate(
.init(
email: "[email protected]",
password: "ChangeMe1"
),
app
)
}

func testAuth() throws {


let app = try createTestApp()
defer { app.shutdown() }

let email = "[email protected]"


let token = try authenticate(
.init(
email: email,
password: "ChangeMe1"
),
app
)
XCTAssertEqual(token.user.email, email)

267
}

func testHomePage() throws {


let app = try createTestApp()
defer { app.shutdown() }

try app.testable(method: .inMemory).test(.GET, "") { res in


XCTAssertEqual(res.status, .ok)

let contentType = try XCTUnwrap(res.headers.contentType)


XCTAssertEqual(contentType, .html)
XCTAssertTrue(res.body.string.contains("Home"))
}
}

func testList() throws {


let app = try createTestApp()
let token = try authenticateRoot(app)
defer { app.shutdown() }

let headers = HTTPHeaders([("Authorization", "Bearer \(token.value)")])

try app
//.testable(method: .inMemory)
.testable(method: .running(port: 8081))
.test(.GET, "/api/blog/categories/", headers: headers) { res in
XCTAssertEqual(res.status, .ok)
let contentType = try XCTUnwrap(res.headers.contentType)
XCTAssertEqual(contentType, .json)
XCTAssertContent([Blog.Category.List].self, res) { content in
XCTAssertEqual(content.count, 4)
}
}
}

func testCreate() throws {


let app = try createTestApp()
let token = try authenticateRoot(app)
defer { app.shutdown() }

let headers = HTTPHeaders([("Authorization", "Bearer \(token.value)")])

let newCategory = Blog.Category.Create(title: "Test category")

try app.test(
.POST,
"/api/blog/categories/",
headers: headers,
content: newCategory
) { res in
XCTAssertEqual(res.status, .created)
let contentType = try XCTUnwrap(res.headers.contentType)
XCTAssertEqual(contentType, .json)
XCTAssertContent(Blog.Category.Detail.self, res) { content in
XCTAssertEqual(content.title, newCategory.title)
}
}
}
}

We've also inserted a new test method for creating a new blog category. This should be
familiar by now. We create a new SUT, request a token, send the request and check the
response using the available helpers from the XCTVapor & XCTest frameworks.

We should refactor the code that we've made so far because the AppTests le is getting a
bit crowded, so let's make a new base class for these new helper methods.

/// FILE: Tests/AppTests/Framework/AppTestCase.swift

268
fi
@testable import App
import XCTVapor

class AppTestCase: XCTestCase {

struct UserLogin: Content {


let email: String
let password: String
}

func createTestApp() throws -> Application {


let app = Application(.testing)

try configure(app)
app.databases.reinitialize()
app.databases.use(.sqlite(.memory), as: .sqlite)
app.databases.default(to: .sqlite)
try app.autoMigrate().wait()
return app
}

func authenticate(
_ user: UserLogin,
_ app: Application
) throws -> User.Token.Detail {
var token: User.Token.Detail?
try app.test(.POST, "/api/sign-in/", beforeRequest: { req in
try req.content.encode(user)
}, afterResponse: { res in
XCTAssertContent(User.Token.Detail.self, res) { content in
token = content
}
})
guard let result = token else {
XCTFail("Login failed")
throw Abort(.unauthorized)
}
return result
}

func authenticateRoot(
_ app: Application
) throws -> User.Token.Detail {
try authenticate(
.init(
email: "[email protected]",
password: "ChangeMe1"
),
app
)
}
}

We can also place the XCTApplicationTester extension into a separate le.

/// FILE: Tests/AppTests/Framework/XCTApplicationTester.swift

import XCTVapor

extension XCTApplicationTester {

@discardableResult public func test<T>(


_ method: HTTPMethod,
_ path: String,
headers: HTTPHeaders = [:],
content: T,
afterResponse: (XCTHTTPResponse) throws -> () = { _ in }
) throws -> XCTApplicationTester where T: Content {
try test(method, path, headers: headers, beforeRequest: { req in
try req.content.encode(content)
}, afterResponse: afterResponse)
}

269
fi
}

Now we can extend this AppTestCase class and take advantage of the utility methods,
keeping only the home page and auth tests inside the AppTests le.

/// FILE: Tests/AppTests/AppTests.swift

@testable import App


import XCTVapor

final class AppTests: AppTestCase {

func testHomePage() throws {


let app = try createTestApp()
defer { app.shutdown() }

try app.testable(method: .inMemory).test(.GET, "") { res in


XCTAssertEqual(res.status, .ok)

let contentType = try XCTUnwrap(res.headers.contentType)


XCTAssertEqual(contentType, .html)
XCTAssertTrue(res.body.string.contains("Home"))
}
}

func testAuth() throws {


let app = try createTestApp()
defer { app.shutdown() }

let email = "[email protected]"


let token = try authenticate(
.init(
email: email,
password: "ChangeMe1"
),
app
)
XCTAssertEqual(token.user.email, email)
}
}

Create a new BlogCategoryApiTests le for the blog category API-related test cases.

/// FILE: Tests/AppTests/BlogCategoryApiTests.swift

@testable import App


import XCTVapor

extension Blog.Category.Create: Content {}

final class BlogCategoryApiTests: AppTestCase {

func testList() throws {


let app = try createTestApp()
let token = try authenticateRoot(app)
defer { app.shutdown() }

let headers = HTTPHeaders([("Authorization", "Bearer \(token.value)")])

try app
//.testable(method: .inMemory)
.testable(method: .running(port: 8081))
.test(.GET, "/api/blog/categories/", headers: headers) { res in
XCTAssertEqual(res.status, .ok)
let contentType = try XCTUnwrap(res.headers.contentType)
XCTAssertEqual(contentType, .json)
XCTAssertContent([Blog.Category.List].self, res) { content in
XCTAssertEqual(content.count, 4)
}

270
fi
fi
}
}

func testCreate() throws {


let app = try createTestApp()
let token = try authenticateRoot(app)
defer { app.shutdown() }

let headers = HTTPHeaders([("Authorization", "Bearer \(token.value)")])

let newCategory = Blog.Category.Create(title: "Test category")

try app.test(
.POST,
"/api/blog/categories/",
headers: headers,
content: newCategory
) { res in
XCTAssertEqual(res.status, .created)
let contentType = try XCTUnwrap(res.headers.contentType)
XCTAssertEqual(contentType, .json)
XCTAssertContent(Blog.Category.Detail.self, res) { content in
XCTAssertEqual(content.title, newCategory.title)
}
}
}
}

This is one possible way of organizing the test cases. If you have lots of test les you can
create directories for modules in the AppTests folder, so the structure will re ect the original
module directory tree. Based on this knowledge you should be able to create tests for the
remaining blog category API endpoints on your own.

It's worth mentioning that you can also chain test methods together if you need to de ne
dependencies between your HTTP calls. For example, after successfully creating, the initial
list should contain 5 elements instead of the original four.

/// FILE: Tests/AppTests/BlogCategoryApiTests.swift

@testable import App


import XCTVapor

extension Blog.Category.Create: Content {}

final class BlogCategoryApiTests: AppTestCase {

func testList() throws {


let app = try createTestApp()
let token = try authenticateRoot(app)
defer { app.shutdown() }

let headers = HTTPHeaders([("Authorization", "Bearer \(token.value)")])

try app
//.testable(method: .inMemory)
.testable(method: .running(port: 8081))
.test(.GET, "/api/blog/categories/", headers: headers) { res in
XCTAssertEqual(res.status, .ok)
let contentType = try XCTUnwrap(res.headers.contentType)
XCTAssertEqual(contentType, .json)
XCTAssertContent([Blog.Category.List].self, res) { content in
XCTAssertEqual(content.count, 4)
}
}
}

func testCreate() throws {

271
fl
fi
fi
let app = try createTestApp()
let token = try authenticateRoot(app)
defer { app.shutdown() }

let headers = HTTPHeaders([("Authorization", "Bearer \(token.value)")])

let newCategory = Blog.Category.Create(title: "Test category")

try app.test(
.POST,
"/api/blog/categories/",
headers: headers,
content: newCategory
) { res in
XCTAssertEqual(res.status, .created)
let contentType = try XCTUnwrap(res.headers.contentType)
XCTAssertEqual(contentType, .json)
XCTAssertContent(Blog.Category.Detail.self, res) { content in
XCTAssertEqual(content.title, newCategory.title)
}
}
}

func testCreateListUpdate() throws {


let app = try createTestApp()
let token = try authenticateRoot(app)
defer { app.shutdown() }

let headers = HTTPHeaders([("Authorization", "Bearer \(token.value)")])

let newCategory = Blog.Category.Create(title: "Test category")

try app
.test(
.POST,
"/api/blog/categories/",
headers: headers,
content: newCategory
) { res in
XCTAssertEqual(res.status, .created)
let contentType = try XCTUnwrap(res.headers.contentType)
XCTAssertEqual(contentType, .json)
XCTAssertContent(Blog.Category.Detail.self, res) { content in
XCTAssertEqual(content.title, newCategory.title)
}
}
.test(
.GET,
"/api/blog/categories/",
headers: headers
) { res in
XCTAssertEqual(res.status, .ok)
let contentType = try XCTUnwrap(res.headers.contentType)
XCTAssertEqual(contentType, .json)
XCTAssertContent([Blog.Category.List].self, res) { content in
XCTAssertEqual(content.count, 5)
}
}
}
}

As you can see, the new XCTVapor framework provides us with many helper tools in unit
testing your server-side Swift application, but couldn't we do something even better?

272
DECLARATIVE UNIT TESTING
Writing unit tests can be quite di cult if you have to deal with multiple closures and type
conversions all the time. Things can get quite complicated real quick, for example, if you
have to re more than one request in a single test scenario.

The XCTVapor library gives us quite handy test methods, but wouldn't be cool if we could
write test cases in a more declarative way? This is what Spec can do for us. It's a very
lightweight and clean open-source library for building declarative unit test cases for Vapor
apps.

/// FILE: Package.swift

// swift-tools-version:5.7
import PackageDescription

let package = Package(


name: "myProject",
platforms: [
.macOS(.v12)
],
dependencies: [
.package(
url: "https://github.com/vapor/vapor",
from: "4.70.0"
),
.package(
url: "https://github.com/vapor/fluent",
from: "4.4.0"
),
.package(
url: "https://github.com/vapor/fluent-sqlite-driver",
from: "4.1.0"
),
.package(
url: "https://github.com/binarybirds/liquid",
from: "1.3.0"
),
.package(
url: "https://github.com/binarybirds/liquid-local-driver",
from: "1.3.0"
),
.package(
url: "https://github.com/binarybirds/swift-html",
from: "1.7.0"
),
.package(
url: "https://github.com/binarybirds/spec",
from: "1.2.0"
),
],
targets: [
.target(name: "App", dependencies: [
.product(name: "Vapor", package: "vapor"),
.product(name: "Fluent", package: "fluent"),
.product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"),
.product(name: "Liquid", package: "liquid"),
.product(name: "LiquidLocalDriver", package: "liquid-local-driver"),
.product(name: "SwiftHtml", package: "swift-html"),
.product(name: "SwiftSvg", package: "swift-html"),
]),
.executableTarget(name: "Run", dependencies: ["App"]),
.testTarget(name: "AppTests", dependencies: [
.target(name: "App"),
.product(name: "XCTVapor", package: "vapor"),
.product(name: "Spec", package: "spec"),

273
fi
ffi
])
]
)

The best way to learn is through practice, so let me show you how to write a similar test
method that we used to check the list blog categories request, but this time using the Spec
framework.

/// FILE: Tests/AppTests/BlogPostApiTests.swift

@testable import App


import XCTVapor
import Spec

extension Blog.Post.Create: Content {}

final class BlogPostApiTests: AppTestCase {

func testList() throws {


let app = try createTestApp()
let token = try authenticateRoot(app)
defer { app.shutdown() }

try app
.describe("Blog posts list API should be fine")
.get("/api/blog/posts/")
.bearerToken(token.value)
.expect(.ok)
.expect(.json)
.expect([Blog.Post.List].self) { content in
XCTAssertEqual(content.count, 9)
}
.test()
}
}

The rst part of the function remains the same, but in the second half we've created a new
Spec object using the .describe function on the app. This will create a new test speci cation
description object for a given test scenario that you can con gure using a builder design
pattern.

You can use the .get method to set the HTTP method with a given path as an argument.
There's also a .on method just like the one that you can use to de ne routes, but Spec gives
us shortcuts for HTTP methods that we tend to use regularly.

The next line sets the authorization header with a bearer token value. You can also set any
kind of HTTP header by using the .header function. The .bearerToken method is just a
shorthand because this is quite a popular header type.

Now here comes the interesting part. We can add expectations to our speci cation through
various .expect methods. First, we expect to have a valid .ok status code with a .json content
type. The last expectation will try to decode the returned data into the given object. If we can
construct the type from the response body, we can access the value inside the block. You
can add extra validations inside the block too. If any code fails, your test will also fail.

The last line will kick o the test, you can also provide the way of execution as a parameter.
The test method can accept the .inMemory and the .running(port:) values, these are the
same test method arguments that we've already seen at the beginning of the chapter.

274
fi
ff
fi
fi
fi
fi
I prefer this approach since it gives us more clarity. Let me show you how to write a test for
creating a new blog post using the Spec library again.

/// FILE: Tests/AppTests/BlogPostApiTests.swift

@testable import App


import XCTVapor
import Spec

extension Blog.Post.Create: Content {}

final class BlogPostApiTests: AppTestCase {

func testList() throws {


let app = try createTestApp()
let token = try authenticateRoot(app)
defer { app.shutdown() }

try app
.describe("Blog posts list API should be fine")
.get("/api/blog/posts/")
.bearerToken(token.value)
.expect(.ok)
.expect(.json)
.expect([Blog.Post.List].self) { content in
XCTAssertEqual(content.count, 9)
}
.test()
}

func testCreate() throws {


let app = try createTestApp()
let token = try authenticateRoot(app)
defer { app.shutdown() }

let newPost = Blog.Post.Create(


title: "Dummy post",
slug: "dummy-slug",
image: "/dummy/image.jpg",
excerpt: "Lorem ipsum",
date: Date(),
content: "Lorem ipsum"
)

try app
.describe("Create post should be fine")
.post("/api/blog/posts/")
.body(newPost)
.bearerToken(token.value)
.expect(.created)
.expect(.json)
.expect(Blog.Post.Detail.self) { content in
XCTAssertEqual(content.title, newPost.title)
}
.test()
}
}

Run the test, but don't be surprised if it fails. As it turns out, we forgot to add a categoryId for
the blog-post-create object. How did we miss it? Let's x it real quick.

/// FILE: Sources/App/Modules/Blog/Objects/BlogPost.swift

import Foundation

extension Blog.Post {

struct List: Codable {


let id: UUID
let title: String

275
fi
let slug: String
let image: String
let excerpt: String
let date: Date
}

struct Detail: Codable {


let id: UUID
let title: String
let slug: String
let image: String
let excerpt: String
let date: Date
let category: Blog.Category.List
let content: String
}

struct Create: Codable {


let title: String
let slug: String
let image: String
let excerpt: String
let date: Date
let content: String
let categoryId: UUID
}

struct Update: Codable {


let title: String
let slug: String
let image: String
let excerpt: String
let date: Date
let content: String
let categoryId: UUID
}

struct Patch: Codable {


let title: String?
let slug: String?
let image: String?
let excerpt: String?
let date: Date?
let content: String?
let categoryId: UUID?
}
}

Back in the unit test implementation, now we have to provide a valid default category. We
can simply query the rst BlogCategoryModel from the database using the regular Fluent
methods, but this will turn our test method into an async method. Fortunately the XCTest
framework now fully supports the new concurrency APIs, so this won't be any trouble: we
just have to mark the method with the async keyword and we're good to go.

/// FILE: Tests/AppTests/BlogPostApiTests.swift

@testable import App


import XCTVapor
import Spec

extension Blog.Post.Create: Content {}

final class BlogPostApiTests: AppTestCase {

func testList() throws {


let app = try createTestApp()
let token = try authenticateRoot(app)
defer { app.shutdown() }

try app

276
fi
.describe("Blog posts list API should be fine")
.get("/api/blog/posts/")
.bearerToken(token.value)
.expect(.ok)
.expect(.json)
.expect([Blog.Post.List].self) { content in
XCTAssertEqual(content.count, 9)
}
.test()
}

func testCreate() async throws {


let app = try createTestApp()
let token = try authenticateRoot(app)
defer { app.shutdown() }

let category = try await BlogCategoryModel.query(on: app.db).first()


guard let category = category else {
XCTFail("Missing default category")
throw Abort(.notFound)
}

let newPost = Blog.Post.Create(


title: "Dummy post",
slug: "dummy-slug",
image: "/dummy/image.jpg",
excerpt: "Lorem ipsum",
date: Date(),
content: "Lorem ipsum",
categoryId: category.id!
)

try app
.describe("Create post should be fine")
.post("/api/blog/posts/")
.body(newPost)
.bearerToken(token.value)
.expect(.created)
.expect(.json)
.expect(Blog.Post.Detail.self) { content in
XCTAssertEqual(content.title, newPost.title)
}
.test()
}
}

Whoops, it failed again; what happened now? We haven't set the referenced category
identi er in the API controller, so let's x this issue real quick.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogPostApiController.swift

import Vapor

extension Blog.Post.List: Content {}


extension Blog.Post.Detail: Content {}

struct BlogPostApiController: ApiController {


typealias ApiModel = Blog.Post
typealias DatabaseModel = BlogPostModel

@AsyncValidatorBuilder
func validators(optional: Bool) -> [AsyncValidator] {
KeyedContentValidator<String>.required("title", optional: optional)
KeyedContentValidator<String>.required("slug", optional: optional)
KeyedContentValidator<String>.required("image", optional: optional)
KeyedContentValidator<String>.required("excerpt", optional: optional)
KeyedContentValidator<String>.required("content", optional: optional)
KeyedContentValidator<UUID>.required("categoryId", optional: optional)
KeyedContentValidator<UUID>("categoryId", "Invalid or missing category",
optional: optional) { value, req in
try await BlogCategoryModel.find(value, on: req.db) != nil

277
fi
fi
}
}

func listOutput(
_ req: Request,
_ models: [DatabaseModel]
) async throws -> [Blog.Post.List] {
models.map { model in
.init(
id: model.id!,
title: model.title,
slug: model.slug,
image: model.imageKey,
excerpt: model.excerpt,
date: model.date
)
}
}

func detailOutput(
_ req: Request,
_ model: DatabaseModel
) async throws -> Blog.Post.Detail {
.init(
id: model.id!,
title: model.title,
slug: model.slug,
image: model.imageKey,
excerpt: model.excerpt,
date: model.date,
category: .init(
id: model.category.id!,
title: model.category.title
),
content: model.content
)
}

func createInput(
_ req: Request,
_ model: DatabaseModel,
_ input: Blog.Post.Create
) async throws {
model.title = input.title
model.slug = input.slug
model.imageKey = input.image
model.excerpt = input.excerpt
model.date = input.date
model.content = input.content
model.$category.id = input.categoryId
}

func updateInput(
_ req: Request,
_ model: DatabaseModel,
_ input: Blog.Post.Update
) async throws {
model.title = input.title
model.slug = input.slug
model.imageKey = input.image
model.excerpt = input.excerpt
model.date = input.date
model.content = input.content
model.$category.id = input.categoryId
}

func patchInput(
_ req: Request,
_ model: DatabaseModel,
_ input: Blog.Post.Patch
) async throws {
model.title = input.title ?? model.title
model.slug = input.slug ?? model.slug
model.imageKey = input.image ?? model.imageKey

278
model.excerpt = input.excerpt ?? model.excerpt
model.date = input.date ?? model.date
model.content = input.content ?? model.content
model.$category.id = input.categoryId ?? model.$category.id
}
}

Try to run the tests again, now they should be ne... or maybe not? Well, seems like the
detailOutput function has some problems too. We've only used that method inside the blog
frontend controller and there we've fetched the category relation with the post.

Turns out it's going to be better if we request the category inside the detail output method,
this will ensure that every time we require a detailed view for a blog post, the related
category will always be available.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogPostApiController.swift

import Vapor

extension Blog.Post.List: Content {}


extension Blog.Post.Detail: Content {}

struct BlogPostApiController: ApiController {


typealias ApiModel = Blog.Post
typealias DatabaseModel = BlogPostModel

@AsyncValidatorBuilder
func validators(optional: Bool) -> [AsyncValidator] {
KeyedContentValidator<String>.required("title", optional: optional)
KeyedContentValidator<String>.required("slug", optional: optional)
KeyedContentValidator<String>.required("image", optional: optional)
KeyedContentValidator<String>.required("excerpt", optional: optional)
KeyedContentValidator<String>.required("content", optional: optional)
KeyedContentValidator<UUID>.required("categoryId", optional: optional)
KeyedContentValidator<UUID>("categoryId", "Invalid or missing category",
optional: optional) { value, req in
try await BlogCategoryModel.find(value, on: req.db) != nil
}
}

func listOutput(
_ req: Request,
_ models: [DatabaseModel]
) async throws -> [Blog.Post.List] {
models.map { model in
.init(
id: model.id!,
title: model.title,
slug: model.slug,
image: model.imageKey,
excerpt: model.excerpt,
date: model.date
)
}
}

func detailOutput(
_ req: Request,
_ model: DatabaseModel
) async throws -> Blog.Post.Detail {
guard
let categoryModel = try await BlogCategoryModel
.find(model.$category.id, on: req.db),
let category = try await BlogCategoryApiController()
.listOutput(req, [categoryModel])
.first
else {
throw Abort(.internalServerError)
}

279
fi
return .init(
id: model.id!,
title: model.title,
slug: model.slug,
image: model.imageKey,
excerpt: model.excerpt,
date: model.date,
category: category,
content: model.content
)
}

func createInput(
_ req: Request,
_ model: DatabaseModel,
_ input: Blog.Post.Create
) async throws {
model.title = input.title
model.slug = input.slug
model.imageKey = input.image
model.excerpt = input.excerpt
model.date = input.date
model.content = input.content
model.$category.id = input.categoryId
}

func updateInput(
_ req: Request,
_ model: DatabaseModel,
_ input: Blog.Post.Update
) async throws {
model.title = input.title
model.slug = input.slug
model.imageKey = input.image
model.excerpt = input.excerpt
model.date = input.date
model.content = input.content
model.$category.id = input.categoryId
}

func patchInput(
_ req: Request,
_ model: DatabaseModel,
_ input: Blog.Post.Patch
) async throws {
model.title = input.title ?? model.title
model.slug = input.slug ?? model.slug
model.imageKey = input.image ?? model.imageKey
model.excerpt = input.excerpt ?? model.excerpt
model.date = input.date ?? model.date
model.content = input.content ?? model.content
model.$category.id = input.categoryId ?? model.$category.id
}
}

Feel free to remove the .with(.$category) line from the BlogFrontendController le.

The create post should work now. Here is another test case that you can use to check if the
update endpoint operates more or less the way it's expected to act.

/// FILE: Tests/AppTests/BlogPostApiTests.swift

@testable import App


import XCTVapor
import Spec

extension Blog.Post.Create: Content {}


extension Blog.Post.Update: Content {}

final class BlogPostApiTests: AppTestCase {

280
fi
func testList() throws {
let app = try createTestApp()
let token = try authenticateRoot(app)
defer { app.shutdown() }

try app
.describe("Blog posts list API should be fine")
.get("/api/blog/posts/")
.bearerToken(token.value)
.expect(.ok)
.expect(.json)
.expect([Blog.Post.List].self) { content in
XCTAssertEqual(content.count, 9)
}
.test()
}

func testCreate() async throws {


let app = try createTestApp()
let token = try authenticateRoot(app)
defer { app.shutdown() }

let category = try await BlogCategoryModel.query(on: app.db).first()


guard let category = category else {
XCTFail("Missing default category")
throw Abort(.notFound)
}

let newPost = Blog.Post.Create(


title: "Dummy post",
slug: "dummy-slug",
image: "/dummy/image.jpg",
excerpt: "Lorem ipsum",
date: Date(),
content: "Lorem ipsum",
categoryId: category.id!
)

try app
.describe("Create post should be fine")
.post("/api/blog/posts/")
.body(newPost)
.bearerToken(token.value)
.expect(.created)
.expect(.json)
.expect(Blog.Post.Detail.self) { content in
XCTAssertEqual(content.title, newPost.title)
}
.test()
}

func testUpdate() async throws {


let app = try self.createTestApp()
let token = try authenticateRoot(app)
defer { app.shutdown() }

guard let post = try await BlogPostModel


.query(on: app.db)
.with(\.$category)
.first()
else {
XCTFail("Missing blog post")
throw Abort(.notFound)
}

let suffix = " updated"

let newPost = Blog.Post.Update(


title: post.title + suffix,
slug: post.slug + suffix,
image: post.imageKey + suffix,
excerpt: post.excerpt + suffix,
date: post.date,

281
content: post.content + suffix,
categoryId: post.category.id!
)

try app
.describe("Update post should be fine")
.put("/api/blog/posts/\(post.id!.uuidString)/")
.body(newPost)
.bearerToken(token.value)
.expect(.ok)
.expect(.json)
.expect(Blog.Post.Detail.self) { content in
XCTAssertEqual(content.id, post.id)
XCTAssertEqual(content.title, newPost.title)
XCTAssertEqual(content.slug, newPost.slug)
XCTAssertEqual(content.image, newPost.image)
XCTAssertEqual(content.excerpt, newPost.excerpt)
XCTAssertEqual(content.content, newPost.content)
}
.test()
}
}

Of course, we could add more validation logic to provide a comprehensive test suite for our
backend server, but we'll stop here. Writing good unit tests is hard, you have to think through
all the possible scenarios and cover as much as you can. Nevertheless, in most cases, you
can reduce a serious amount of bugs with just a few good test cases.

SUMMARY
In this chapter we've seen how to work with test targets using the Swift Package Manager
and Xcode. The rst half of the chapter was about getting familiar with the XCTVapor library
which provides utility tools for writing test cases. A good server should be bulletproof: that's
why it's really important to have high test coverage, but still, creating lots of di erent unit
tests is hard. This is where the Spec framework can help you by using declarations.

282
fi
ff
CHAPTER 15:
EVENT DRIVEN HOOK FUNCTIONS

In this chapter, we're going to eliminate the dependencies between the modules by
introducing a brand new event-driven architecture. By using hook functions we're going to
be able to build connections without the need of importing the interface of a module into
another. The EDA design pattern allows us to create loosely coupled software components
and services without forming an actual dependency between the participants.

GENERIC HOOK FUNCTIONS


Let's start by creating the new folders we’ll use during this chapter. You can use the
commands below to add them:

cd ~/myProject
mkdir -p Sources/App/Framework/Hooks
mkdir -p Sources/App/Framework/Hooks/Sync
mkdir -p Sources/App/Framework/Hooks/Async

The issue with our codebase that we want to solve is that we've hardcoded blog-related
menu items into the admin module. This isn't ideal, because what if we don't want to use the
blog module at all? Well, we also have to change the admin module to remove the
dependency. This would work ne in some cases, but ideally, we want to be able to build
completely independent modules.

For this purpose, we're going to introduce hook functions. A hook is a place in your code
that allows developers to tap into a module. By using hooks it's possible to provide a custom
implementation for a given behavior or react to an event when something happens.

Since we'd like to pass around various hook arguments, we can de ne an alias for this
purpose. It's just going to be a simple dictionary with a String key and an Any value.

/// FILE: Sources/App/Framework/Hooks/HookArguments.swift

typealias HookArguments = [String: Any]

Now we can de ne a protocol to invoke hook functions. Invocation means calling a function
pointer that we're going to store somewhere else for later use. We can also de ne a custom
type alias for a generic HookFunctionSignature type, this will simplify things a bit.

/// FILE: Sources/App/Framework/Hooks/Sync/HookArguments.swift

protocol HookFunction {
func invoke(_: HookArguments) -> Any
}

typealias HookFunctionSignature<T> = (HookArguments) -> T

283
fi
fi
fi
fi
A hook function is just a protocol that allows us to invoke the object that implements this
interface. We still have to create a type-erased AnyHookFunction, because in Swift generics
have some restrictions when you work with protocols with associated types.

The AnyHookFunction is going to be a struct that can hold a function pointer with an Any
return type, we can use this object to erase a custom HookFunction and later on convert
back the Any return type to the required value if possible.

/// FILE: Sources/App/Framework/Hooks/Sync/AnyHookFunction.swift

struct AnyHookFunction: HookFunction {

private let functionBlock: HookFunctionSignature<Any>

init(_ functionBlock: @escaping HookFunctionSignature<Any>) {


self.functionBlock = functionBlock
}

func invoke(_ args: HookArguments) -> Any {


functionBlock(args)
}
}

In other words, the AnyHookFunction is just a helper that we can use to wrap or box a hook
function signature block when we register a new hook function. Otherwise, without an Any
wrapper, we'd have to create additional objects, but with the help of the AnyHookFunction
we can simply use a block and box the original pointer.

We're going to de ne a HookFunctionPointer class that will contain the associated name,
the pointer, and the return type. We're going to store AnyHookFunction objects as pointer
values.

/// FILE: Sources/App/Framework/Hooks/HookFunctionPointer.swift

final class HookFunctionPointer<Pointer> {

var name: String


var pointer: Pointer
var returnType: Any.Type

init(
name: String,
function: Pointer,
returnType: Any.Type
) {
self.name = name
self.pointer = function
self.returnType = returnType
}
}

The HookFunctionPointer is going to be used inside the hook storage: that's the shared
storage block for this entire system. The hook storage is the place where all your event
handlers live and you can call these events through this storage pointer when you need to
trigger an event.

/// FILE: Sources/App/Framework/Hooks/HookStorage.swift

final class HookStorage {

private var pointers: [HookFunctionPointer<HookFunction>]

284
fi
init() {
self.pointers = []
}

The HookStorage requires just a few more additional methods.

The register function will allow developers to register a function using a name and a return
type. Then we have to erase the return type using an AnyHookFunction since we can only
store objects with the same types in the pointers array. Next, we create an actual
HookFunctionPointer with the associated name and return type.

The invoke and invokeAll methods will check if there's a function pointer with a given name
and return type inside the pointers array, and if there's a match, it'll invoke the underlying
object. The invokeAll method will call every single handler and return with a result array, but
the invoke method will just call the very rst one and return with an optional result.

/// FILE: Sources/App/Framework/Hooks/Sync/HookStorage+Hooks.swift

extension HookStorage {

func register<ReturnType>(
_ name: String,
use block: @escaping HookFunctionSignature<ReturnType>
) {
let function = AnyHookFunction { args -> Any in
block(args)
}
let pointer = HookFunctionPointer<HookFunction>(
name: name,
function: function,
returnType: ReturnType.self
)
pointers.append(pointer)
}

func invoke<ReturnType>(
_ name: String,
args: HookArguments = [:]
) -> ReturnType? {
pointers.first {
$0.name == name && $0.returnType == ReturnType.self
}?.pointer.invoke(args) as? ReturnType
}

func invokeAll<ReturnType>(
_ name: String, args: HookArguments = [:]
) -> [ReturnType] {
pointers.filter {
$0.name == name && $0.returnType == ReturnType.self
}
.compactMap {
$0.pointer.invoke(args) as? ReturnType
}
}
}

We can use Vapor's application storage to make hook storage available globally. We can
check if there's existing storage and return that, this way it'll work like a singleton. Don't
worry; for this purpose, it's completely ne to use a singleton since this is a shared hook
pointer storage that we'd like to use thorough the entire app.

285
fi
fi
/// FILE: Sources/App/Framework/Hooks/Application+HookStorage.swift

import Vapor

extension Application {

private struct HookStorageKey: StorageKey {


typealias Value = HookStorage
}

var hooks: HookStorage {


get {
if let existing = storage[HookStorageKey.self] {
return existing
}
let new = HookStorage()
storage[HookStorageKey.self] = new
return new
}
set {
storage[HookStorageKey.self] = newValue
}
}
}

We can also de ne helper extensions on the Application; this way we can directly invoke
hooks, plus we can pass a reference to the app itself using the hook arguments.

/// FILE: Sources/App/Framework/Hooks/Sync/Application+Hooks.swift

import Vapor

extension Application {

func invoke<ReturnType>(
_ name: String,
args: HookArguments = [:]
) -> ReturnType? {
let ctxArgs = args.merging(["app": self]) { (_, new) in new }
return hooks.invoke(name, args: ctxArgs)
}

func invokeAll<ReturnType>(
_ name: String,
args: HookArguments = [:]
) -> [ReturnType] {
let ctxArgs = args.merging(["app": self]) { (_, new) in new }
return hooks.invokeAll(name, args: ctxArgs)
}
}

We can do the same for the Request objects: pass the request pointer as a hook argument
and merge the new arguments with the old ones while making sure that other parameters
will be kept.

/// FILE: Sources/App/Framework/Hooks/Sync/Request+Hooks.swift

import Vapor

extension Request {

func invoke<ReturnType>(
_ name: String,
args: HookArguments = [:]
) -> ReturnType? {
let ctxArgs = args.merging(["req": self]) { (_, new) in new }
return application.invoke(name, args: ctxArgs)
}

286
fi
func invokeAll<ReturnType>(
_ name: String,
args: HookArguments = [:]
) -> [ReturnType] {
let ctxArgs = args.merging(["req": self]) { (_, new) in new }
return application.invokeAll(name, args: ctxArgs)
}
}

Now we're ready to use the newly created hook system. I know it's a bit hard to follow this
section with all the generics involved, but don't worry too much about it since we're only
going to use the register and invoke methods from now on.

The register function allows us to register a function pointer with the invoke method that
makes it possible to call that stored function pointer at some point in the future.

If you know how these methods work under the hood, that's a huge plus, but not crucial.

USING THE HOOK SYSTEM


Let's try out the new hook system, by altering the AdminDashboardTemplate. We can use
the invokeAll method to get back an array of TemplateRepresentable objects that we can
use to render small widgets on the dashboard. This is how you can invoke a set of hooks.

/// FILE: Sources/App/Modules/Admin/Templates/Html/AdminDashboardTemplate.swift

import Vapor
import SwiftHtml

struct AdminDashboardTemplate: TemplateRepresentable {

var context: AdminDashboardContext

init(
_ context: AdminDashboardContext
) {
self.context = context
}

@TagBuilder
func render(
_ req: Request
) -> Tag {
AdminIndexTemplate(
.init(title: context.title)
) {
Div {
Section {
P(context.icon)
H1(context.title)
P(context.message)
}

Div {
let widgets: [TemplateRepresentable] = req.invokeAll("admin-widget")
widgets.map { $0.render(req) }
}
.class("widgets")
}
.id("dashboard")
.class("container")

287
}
.render(req)
}
}

Now we just need a widget template that we can return as a hook response from the blog
module.

/// FILE: Sources/App/Modules/Blog/Templates/Html/BlogAdminWidgetTemplate.swift

import Vapor
import SwiftHtml

struct BlogAdminWidgetTemplate: TemplateRepresentable {

@TagBuilder
func render(_ req: Request) -> Tag {
H2("Blog")
Ul {
Li {
A("Posts")
.href("/admin/blog/posts/")
}
Li {
A("Categories")
.href("/admin/blog/categories/")
}
}
}
}

Inside the module, we should register the "admin-widget" hook function. Every single hook
function will have the same HookArguments parameter and the return type is de ned when
the invocation happens. If you don't return with a proper type your hook won't be called.

For the adminWidgetHook method we have to return with a TemplateRepresentable object.

/// FILE: Sources/App/Modules/Blog/BlogModule.swift

import Vapor

struct BlogModule: ModuleInterface {

let router = BlogRouter()

func boot(_ app: Application) throws {


app.migrations.add(BlogMigrations.v1())
app.migrations.add(BlogMigrations.seed())

app.hooks.register("admin-widget", use: adminWidgetHook)

try router.boot(routes: app.routes)


}

func adminWidgetHook(
_ args: HookArguments
) -> TemplateRepresentable {
BlogAdminWidgetTemplate()
}
}

Run the app and hopefully you should see that the admin dashboard still works and the links
to the blog module objects are still there. You can try to de ne multiple widgets: for example,
you can have the same functionality for the user module to display user-related links on the
dashboard.

288
fi
fi
ENHANCED ROUTING SYSTEM
The other issue with our project is that the routing system follows a static approach. This
means that inside the blog router we had to group the base routes for the admin and API
outputs and use the redirect and guard middlewares to protect those endpoints.

Fortunately, we have a hook system, so we can improve this too. First of all, since the route
registration happens in the boot method we need a new setUp function that's going to be
called after every module has been booted up, this way we can make sure that all the hook
functions are registered by the time we set up additional routes using hook invocation
points.

/// FILE: Sources/App/Framework/ModuleInterface.swift

import Vapor

public protocol ModuleInterface {

static var identifier: String { get }

func boot(_ app: Application) throws


func setUp(_ app: Application) throws
}

public extension ModuleInterface {

func boot(_ app: Application) throws {}


func setUp(_ app: Application) throws {}

static var identifier: String {


String(describing: self).dropLast(6).lowercased()
}
}

Inside the con guration le, we simply call the setUp method after the boot procedure.

/// FILE: Sources/App/con gure.swift

import Vapor
import Fluent
import FluentSQLiteDriver
import Liquid
import LiquidLocalDriver

public func configure(


_ app: Application
) throws {

app.fileStorages.use(
.local(
publicUrl: "http://localhost:8080",
publicPath: app.directory.publicDirectory,
workDirectory: "assets"
),
as: .local
)

app.routes.defaultMaxBodySize = "10mb"

let dbPath = app.directory.resourcesDirectory + "db.sqlite"


app.databases.use(.sqlite(.file(dbPath)), as: .sqlite)

app.middleware.use(
FileMiddleware(

289
fi
fi
fi
publicDirectory: app.directory.publicDirectory
)
)

app.middleware.use(ExtendPathMiddleware())

app.sessions.use(.fluent)
app.migrations.add(SessionRecord.migration)
app.middleware.use(app.sessions.middleware)

let modules: [ModuleInterface] = [


WebModule(),
UserModule(),
AdminModule(),
ApiModule(),
BlogModule(),
]

for module in modules {


try module.boot(app)
}

for module in modules {


try module.setUp(app)
}

try app.autoMigrate().wait()
}

Now we can move the admin-related logic into the AdminRouter. We can create a
setUpRoutesHooks function to group the core routes and protect them, then we can pass
the new routes pointer as a hook argument and call every single hook function that uses the
"admin-routes" name.

The admin router can also use this hook to register the dashboard handler function. As you
can see, we can simply force cast the RoutesBuilder using the routes key from the
arguments. It's safe to use a force here because without a routes pointer the entire hook
would be useless.

/// FILE: Sources/App/Modules/Admin/AdminRouter.swift

import Vapor

struct AdminRouter: RouteCollection {

let controller = AdminFrontendController()

func boot(routes: RoutesBuilder) throws {}

func setUpRoutesHooks(app: Application) throws {


let adminRoutes = app.routes
.grouped(
AuthenticatedUser.redirectMiddleware(
path: "/sign-in/"
)
)
.grouped("admin")

let _: [Void] = app.invokeAll(


"admin-routes",
args: ["routes": adminRoutes]
)
}

func adminRoutesHook(_ args: HookArguments) -> Void {


let routes = args["routes"] as! RoutesBuilder

routes.get(use: controller.dashboardView)

290
}
}

In the AdminModule, we can call register the new admin routes hook using the router
instance, then inside the setUp method, we can call the setUpRoutesHooks function.

/// FILE: Sources/App/Modules/Admin/AdminModule.swift

import Vapor

struct AdminModule: ModuleInterface {

let router = AdminRouter()

func boot(_ app: Application) throws {


try router.boot(routes: app.routes)

app.hooks.register("admin-routes", use: router.adminRoutesHook)


}

func setUp(_ app: Application) throws {


try router.setUpRoutesHooks(app: app)
}
}

We can now refactor the BlogRouter to take advantage of the new admin routes hook
system.

/// FILE: Sources/App/Modules/Blog/BlogRouter.swift

import Vapor

struct BlogRouter: RouteCollection {

let frontendController = BlogFrontendController()

let postAdminController = BlogPostAdminController()


let postApiController = BlogPostApiController()

let categoryAdminController = BlogCategoryAdminController()


let categoryApiController = BlogCategoryApiController()

func boot(routes: RoutesBuilder) throws {


routes.get("blog", use: frontendController.blogView)
routes.get(.anything, use: frontendController.postView)

let api = routes


.grouped(AuthenticatedUser.guardMiddleware())
.grouped("api")
postApiController.setupRoutes(api)
categoryApiController.setupRoutes(api)
}

func adminRoutesHook(_ args: HookArguments) -> Void {


let routes = args["routes"] as! RoutesBuilder

postAdminController.setupRoutes(routes)
categoryAdminController.setupRoutes(routes)
}
}

Finally, we just have to register our hook function so the system can call the blog router's
adminRoutesHook method when it's needed.

/// FILE: Sources/App/Modules/Blog/BlogModule.swift

import Vapor

291
struct BlogModule: ModuleInterface {

let router = BlogRouter()

func boot(_ app: Application) throws {


app.migrations.add(BlogMigrations.v1())
app.migrations.add(BlogMigrations.seed())

app.hooks.register("admin-widget", use: adminWidgetHook)


app.hooks.register("admin-routes", use: router.adminRoutesHook)

try router.boot(routes: app.routes)


}

func adminWidgetHook(
_ args: HookArguments
) -> TemplateRepresentable {
BlogAdminWidgetTemplate()
}
}

We can apply the same principles to the API routes. Let's build a new ApiRouter for this
purpose.

/// FILE: Sources/App/Modules/Api/ApiRouter.swift

import Vapor

struct ApiRouter: RouteCollection {

func boot(routes: RoutesBuilder) throws {}

func setUpRoutesHooks(app: Application) throws {


let apiRoutes = app.routes
.grouped(AuthenticatedUser.guardMiddleware())
.grouped("api")

let _: [Void] = app.invokeAll(


"api-routes",
args: ["routes": apiRoutes]
)
}
}

We should alter the ApiModule le and call the setup method on the router.

/// FILE: Sources/App/Modules/Api/ApiModule.swift

import Vapor

struct ApiModule: ModuleInterface {

let router = ApiRouter()

func boot(_ app: Application) throws {


app.middleware.use(ApiErrorMiddleware())

try router.boot(routes: app.routes)


}

func setUp(_ app: Application) throws {


try router.setUpRoutesHooks(app: app)
}
}

Now we can go back to the BlogRouter and de ne an apiRoutesHook function.

292
fi
fi
/// FILE: Sources/App/Modules/Blog/BlogRouter.swift

import Vapor

struct BlogRouter: RouteCollection {

let frontendController = BlogFrontendController()

let postAdminController = BlogPostAdminController()


let postApiController = BlogPostApiController()

let categoryAdminController = BlogCategoryAdminController()


let categoryApiController = BlogCategoryApiController()

func boot(routes: RoutesBuilder) throws {


routes.get("blog", use: frontendController.blogView)
routes.get(.anything, use: frontendController.postView)
}

func adminRoutesHook(_ args: HookArguments) -> Void {


let routes = args["routes"] as! RoutesBuilder

postAdminController.setupRoutes(routes)
categoryAdminController.setupRoutes(routes)
}

func apiRoutesHook(_ args: HookArguments) -> Void {


let routes = args["routes"] as! RoutesBuilder

postApiController.setupRoutes(routes)
categoryApiController.setupRoutes(routes)
}
}

As a very last step, we have to register our handler to listen for the "api-routes" event.

/// FILE: Sources/App/Modules/Blog/BlogModule.swift

import Vapor

struct BlogModule: ModuleInterface {

let router = BlogRouter()

func boot(_ app: Application) throws {


app.migrations.add(BlogMigrations.v1())
app.migrations.add(BlogMigrations.seed())

app.hooks.register("admin-widget", use: adminWidgetHook)


app.hooks.register("admin-routes", use: router.adminRoutesHook)
app.hooks.register("api-routes", use: router.apiRoutesHook)

try router.boot(routes: app.routes)


}

func adminWidgetHook(
_ args: HookArguments
) -> TemplateRepresentable {
BlogAdminWidgetTemplate()
}
}

This is how you can hook into various events and provide hook functions to provide custom
behavior when it's needed. This way you don't have to import header les from one module
to another, but you can have a completely decoupled module system.

293
fi
I believe this solution has its pros and cons, but in the long term, it's worthwhile to consider
an event-driven architecture, especially if you'd like to enable or disable modules at runtime.

THE ASYNCHRONOUS HOOK SYSTEM


So far we were able to call synchronous hook methods, but since most of the Vapor and
Fluent APIs are async, we might encounter a scenario when we have to register an async
hook function.

In this section, we're going to build a dynamic async hook function that can be used to
respond to a given route path. This way not only the blog module can catch the .anything
route, but other modules can hook into this as well.

First of all, we're going to change the postView in the BlogFrontendController; if you don't
remember why we've created this method, please revisit Chapter 2 for more info. Originally
we've returned with a Response object since if we haven't found a post with a given slug, we
redirected the website to the home page.

This wasn't ideal and from an SEO perspective: you might want to return a proper 404 page
instead of a 301 redirection when you enter a page URL that isn't available on your site. We
can x this via hook functions. First of all, we have to change the Response to an optional
value and return nil if we weren't able to nd a post-entry inside the database.

/// FILE: Sources/App/Modules/Blog/Controllers/BlogFrontendController.swift

import Vapor
import Fluent

struct BlogFrontendController {

func blogView(req: Request) async throws -> Response {


let posts = try await BlogPostModel
.query(on: req.db)
.sort(\.$date, .descending)
.all()

let api = BlogPostApiController()


let listOutput = try await api.listOutput(req, posts)

let ctx = BlogPostsContext(


icon: "🔥 ",
title: "Blog",
message: "Hot news and stories about everything.",
posts: listOutput
)

return req.templates.renderHtml(BlogPostsTemplate(ctx))
}

func postView(
_ req: Request
) async throws -> Response? {
let slug = req.url.path.trimmingCharacters(
in: .init(charactersIn: "/")
)
guard
let post = try await BlogPostModel
.query(on: req.db)

294
fi
fi
.filter(\.$slug == slug)
.first()
else {
return nil
}
let model = try await BlogPostApiController().detailOutput(req, post)
let context = BlogPostContext(post: model)
return req.templates.renderHtml(BlogPostTemplate(context))
}
}

Inside the BlogRouter we can remove the original get .anything route registration and we're
going to prepare an async hook function that we're going to register as a "response" hook.
This hook function will be called with a request object, so we can cast the pointer and simply
call the postView method on the frontend controller to provide the optional response.

/// FILE: Sources/App/Modules/Blog/BlogRouter.swift

import Vapor

struct BlogRouter: RouteCollection {

let frontendController = BlogFrontendController()

let postAdminController = BlogPostAdminController()


let postApiController = BlogPostApiController()

let categoryAdminController = BlogCategoryAdminController()


let categoryApiController = BlogCategoryApiController()

func boot(routes: RoutesBuilder) throws {


routes.get("blog", use: frontendController.blogView)
}

func adminRoutesHook(_ args: HookArguments) -> Void {


let routes = args["routes"] as! RoutesBuilder

postAdminController.setupRoutes(routes)
categoryAdminController.setupRoutes(routes)
}

func apiRoutesHook(_ args: HookArguments) -> Void {


let routes = args["routes"] as! RoutesBuilder

postApiController.setupRoutes(routes)
categoryApiController.setupRoutes(routes)
}

func responseHook(
_ args: HookArguments
) async throws -> Response? {
let req = args["req"] as! Request
return try await frontendController.postView(req)
}
}

Now we should register the response hook inside the module le, but we'll have a little
problem with that. Since the responseHook function is marked with an async keyword, it has
a di erent function signature and our hook system isn't able to process async functions just
yet.

/// FILE: Sources/App/Modules/Blog/BlogModule.swift

import Vapor

struct BlogModule: ModuleInterface {

295
ff
fi
let router = BlogRouter()

func boot(_ app: Application) throws {


app.migrations.add(BlogMigrations.v1())
app.migrations.add(BlogMigrations.seed())

app.hooks.register("admin-widget", use: adminWidgetHook)


app.hooks.register("admin-routes", use: router.adminRoutesHook)
app.hooks.register("api-routes", use: router.apiRoutesHook)
app.hooks.register("response", use: router.responseHook)

try router.boot(routes: app.routes)


}

func adminWidgetHook(
_ args: HookArguments
) -> TemplateRepresentable {
BlogAdminWidgetTemplate()
}
}

We can x this problem by introducing an AsyncHookFunction protocol and an


AsyncHookFunctionSignature alias for asynchronous hook variants.

/// FILE: Sources/App/Framework/Hooks/Async/AsyncHookFunction.swift

protocol AsyncHookFunction {
func invokeAsync(_: HookArguments) async throws -> Any
}

typealias AsyncHookFunctionSignature<T> = (HookArguments) async throws -> T

Just like we did at the beginning of this chapter we should build a type eraser object called
AsyncAnyHookFunction to hide the return type of kind of async hook methods.

/// FILE: Sources/App/Framework/Hooks/Async/AsyncAnyHookFunction.swift

struct AsyncAnyHookFunction: AsyncHookFunction {

private let functionBlock: AsyncHookFunctionSignature<Any>

init(_ functionBlock: @escaping AsyncHookFunctionSignature<Any>) {


self.functionBlock = functionBlock
}

func invokeAsync(_ args: HookArguments) async throws -> Any {


try await functionBlock(args)
}
}

Inside the HookStorage class, we now have to use two separate pointers, one for the
regular sync hook functions and another for the async versions. This seems like a hacky
solution, but unfortunately, we can't do much about it, because Swift's async overload
resolution only cares about the calling context of the function and we don't want to introduce
even more dirty tricks to solve this issue. It's completely ne to have two separate arrays.

/// FILE: Sources/App/Framework/Hooks/HookStorage.swift

final class HookStorage {

var pointers: [HookFunctionPointer<HookFunction>]


var asyncPointers: [HookFunctionPointer<AsyncHookFunction>]

init() {
self.pointers = []

296
fi
fi
self.asyncPointers = []
}
}

We can use an Async su x to indicate that we'd like to register or call the asynchronous
version of a given hook. Several other libraries follow the same approach for compatibility
reasons; just remember that AsyncMiddleware protocol from Vapor.

/// FILE: Sources/App/Framework/Hooks/Async/HookStorage+AsyncHooks.swift

extension HookStorage {

func registerAsync<ReturnType>(
_ name: String,
use block: @escaping AsyncHookFunctionSignature<ReturnType>
) {
let function = AsyncAnyHookFunction { args -> Any in
try await block(args)
}
let pointer = HookFunctionPointer<AsyncHookFunction>(
name: name,
function: function,
returnType: ReturnType.self
)
asyncPointers.append(pointer)
}

func invokeAsync<ReturnType>(
_ name: String,
args: HookArguments = [:]
) async throws -> ReturnType? {
try await asyncPointers.first {
$0.name == name && $0.returnType == ReturnType.self
}?.pointer.invokeAsync(args) as? ReturnType
}

func invokeAllAsync<ReturnType>(
_ name: String,
args: HookArguments = [:]
) async throws -> [ReturnType] {
let fns = asyncPointers.filter {
$0.name == name && $0.returnType == ReturnType.self
}
var result: [ReturnType] = []
for fn in fns {
if let res = try await fn.pointer.invokeAsync(args) as? ReturnType {
result.append(res)
}
}
return result
}
}

Now we can update the BlogModule to use our new async function.

/// FILE: Sources/App/Modules/Blog/BlogModule.swift

import Vapor

struct BlogModule: ModuleInterface {

let router = BlogRouter()

func boot(_ app: Application) throws {


app.migrations.add(BlogMigrations.v1())
app.migrations.add(BlogMigrations.seed())

app.hooks.register("admin-widget", use: adminWidgetHook)


app.hooks.register("admin-routes", use: router.adminRoutesHook)

297
ffi
app.hooks.register("api-routes", use: router.apiRoutesHook)
app.hooks.registerAsync("response", use: router.responseHook)

try router.boot(routes: app.routes)


}

func adminWidgetHook(
_ args: HookArguments
) -> TemplateRepresentable {
BlogAdminWidgetTemplate()
}
}

Just like we did before we should add our async helpers to the Application extension.

/// FILE: Sources/App/Framework/Hooks/Async/Application+AsyncHooks.swift

import Vapor

extension Application {

func invokeAsync<ReturnType>(
_ name: String,
args: HookArguments = [:]
) async throws -> ReturnType? {
let ctxArgs = args.merging(["app": self]) { (_, new) in new }
return try await hooks.invokeAsync(name, args: ctxArgs)
}

func invokeAllAsync<ReturnType>(
_ name: String,
args: HookArguments = [:]
) async throws -> [ReturnType] {
let ctxArgs = args.merging(["app": self]) { (_, new) in new }
return try await hooks.invokeAllAsync(name, args: ctxArgs)
}
}

The same thing applies to the Request object; these helpers are quite handy since they'll
ensure that we won't forget to pass around the necessary pointers.

/// FILE: Sources/App/Framework/Hooks/Async/Request+AsyncHooks.swift

import Vapor

extension Request {

func invokeAsync<ReturnType>(
_ name: String,
args: HookArguments = [:]
) async throws -> ReturnType? {
let ctxArgs = args.merging(["req": self]) { (_, new) in new }
return try await application.invokeAsync(name, args: ctxArgs)
}

func invokeAllAsync<ReturnType>(
_ name: String,
args: HookArguments = [:]
) async throws -> [ReturnType] {
let ctxArgs = args.merging(["req": self]) { (_, new) in new }
return try await application.invokeAllAsync(name, args: ctxArgs)
}
}

Now inside the WebFrontendController we should create a new responder function that'll
take care of the GET .anything route. Inside this anyResponse method, we can call the async

298
"response" hooks, and if there was a non-nil response we can return with that, otherwise, we
can simply throw a .notFound error.

Please note that you could respond with a custom template that has a proper 404 response
page instead of throwing an Abort, but for the sake of simplicity, we're going to ignore that
for now.

/// FILE: Sources/App/Modules/Web/Controllers/WebFrontendController.swift

import Vapor

struct WebFrontendController {

func anyResponse(
_ req: Request
) async throws -> Response {
let result: [Response?] = try await req.invokeAllAsync("response")
guard let response = result.compactMap({ $0 }).first else {
throw Abort(.notFound)
}
return response
}

func homeView(req: Request) throws -> Response {


let ctx = WebHomeContext(
icon: "👋 ",
title: "Home",
message: "Hi there, welcome to my page.",
paragraphs: [
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
"Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.",
"Nisi ut aliquip ex ea commodo consequat.",
],
link: .init(
label: "Read my blog →",
url: "/blog/"
)
)

return req.templates.renderHtml(
WebHomeTemplate(ctx)
)
}
}

In the WebRouter we should use the anyResponse method; this is going to take place inside
a custom setUpRoutesHooks function, just like we did for the admin and API modules.

/// FILE: Sources/App/Modules/Web/WebRouter.swift

import Vapor

struct WebRouter: RouteCollection {

let controller = WebFrontendController()

func boot(routes: RoutesBuilder) throws {


routes.get(use: controller.homeView)
}

func setUpRoutesHooks(app: Application) throws {


app.routes.get(.anything, use: controller.anyResponse)
}
}

299
The very last step in this chapter is to implement the setUp method inside the web module.

/// FILE: Sources/App/Modules/Web/WebModule.swift

import Vapor

struct WebModule: ModuleInterface {

let router = WebRouter()

func boot(_ app: Application) throws {


try router.boot(routes: app.routes)
}

func setUp(_ app: Application) throws {


try router.setUpRoutesHooks(app: app)
}
}

This con guration will allow other modules to respond to a given path if they can provide
content for that URL. For example, a news module could hook up to the response event and
render a news page if the path matches the given criteria.

This is a great way of building SEO-friendly decoupled path handlers.

SUMMARY
Hook functions are extremely powerful; there are quite a lot of other use-cases, but this
chapter was more about learning the basics of the event-driven architecture and getting
familiar with the hook registration and invocation process.

300
fi
CHAPTER 16:
SHARED API LIBRARY PACKAGES

In this chapter, we're going to separate the data transfer object layer into a standalone Swift
package product, this way you'll be able to share server-side Swift code with client apps. In
the rst part of the chapter, I'm going to show you how to set up the project and we're going
to add access control modi ers to allow other modules to see our DTOs. The second half of
the chapter is going to give you some really basic examples of how to perform HTTP
requests using the modern Swift concurrency APIs.

CREATING A SHARED API LIBRARY


Let's start by creating the new folders we’ll use during this chapter. You can use the
commands below to add them:

cd ~/myProject
mkdir -p Sources/AppApi
mkdir -p Sources/AppApi/Framework
mkdir -p Sources/AppApi/Modules
mkdir -p Sources/AppApi/Modules/Blog
mkdir -p Sources/AppApi/Modules/User

First of all, we should de ne a brand new target and a public library product for the API layer.
This will make it possible for others to import the AppApi target as a package dependency.
This target will contain all the shared DTOs plus the two API interface les.

// FILE: Package.swift

// swift-tools-version:5.7
import PackageDescription

let package = Package(


name: "myProject",
platforms: [
.macOS(.v12)
],
products: [
.library(name: "AppApi", targets: ["AppApi"]),
],
dependencies: [
.package(
url: "https://github.com/vapor/vapor",
from: "4.70.0"
),
.package(
url: "https://github.com/vapor/fluent",
from: "4.4.0"
),
.package(
url: "https://github.com/vapor/fluent-sqlite-driver",
from: "4.1.0"
),
.package(
url: "https://github.com/binarybirds/liquid",

301
fi
fi
fi
fi
from: "1.3.0"
),
.package(
url: "https://github.com/binarybirds/liquid-local-driver",
from: "1.3.0"
),
.package(
url: "https://github.com/binarybirds/swift-html",
from: "1.7.0"
),
.package(
url: "https://github.com/binarybirds/spec",
from: "1.2.0"
),
],
targets: [
.target(name: "AppApi", dependencies: []),
.target(name: "App", dependencies: [
.product(name: "Vapor", package: "vapor"),
.product(name: "Fluent", package: "fluent"),
.product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"),
.product(name: "Liquid", package: "liquid"),
.product(name: "LiquidLocalDriver", package: "liquid-local-driver"),
.product(name: "SwiftHtml", package: "swift-html"),
.product(name: "SwiftSvg", package: "swift-html"),
.target(name: "AppApi")
]),
.executableTarget(name: "Run", dependencies: ["App"]),
.testTarget(name: "AppTests", dependencies: [
.target(name: "App"),
.product(name: "XCTVapor", package: "vapor"),
.product(name: "Spec", package: "spec"),
])
]
)

We should move the ApiModelInterface to the Sources/AppApi/Framework directory. This


target won't have Vapor as a dependency, so we should remove the pathIdComponent
variable for now, because the PathComponent type is part of the Vapor framework.

// FILE: Sources/AppApi/Framework/ApiModelInterface.swift

public protocol ApiModelInterface {


associatedtype Module: ApiModuleInterface

static var pathKey: String { get }


static var pathIdKey: String { get }
}

public extension ApiModelInterface {

static var pathKey: String {


String(describing: self).lowercased() + "s"
}

static var pathIdKey: String {


String(describing: self).lowercased() + "Id"
}
}

Under the Sources/App/Framework directory, we can create a new extension for the
ApiModelInterface and place the original pathIdComponent property de nition there.

// FILE: Sources/App/Framework/ApiModelInterface+PathComponent.swift

import AppApi

extension ApiModelInterface {

302
fi
static var pathIdComponent: PathComponent {
.init(stringLiteral: ":" + pathIdKey)
}
}

Let's move the ApiModuleInterface inside the AppApi/Framework folder.

// FILE: Sources/AppApi/Framework/ApiModuleInterface.swift

public protocol ApiModuleInterface {


static var pathKey: String { get }
}

public extension ApiModuleInterface {

static var pathKey: String {


String(describing: self).lowercased()
}
}

The data transfer objects should also be moved, so they can be part of this new target. We
also have to de ne these objects as publicly available; otherwise, they won't be visible
outside of the module scope and others can't use them.

// FILE: Sources/AppApi/Modules/Blog/Blog.swift

public enum Blog: ApiModuleInterface {

public enum Post: ApiModelInterface {


public typealias Module = Blog
}

public enum Category: ApiModelInterface {


public typealias Module = Blog

public static let pathKey: String = "categories"


}
}

When you create a public extension, fortunately, you don't have to explicitly mark underlying
objects as public; they'll have public access control level by default. On the other hand, auto-
generated init methods for a struct are internal by default, so we're going to explicitly create
them now.

// FILE: Sources/AppApi/Modules/Blog/BlogCategory.swift

import Foundation

public extension Blog.Category {

struct List: Codable {


public let id: UUID
public let title: String

public init(
id: UUID,
title: String
) {
self.id = id
self.title = title
}
}

struct Detail: Codable {


public let id: UUID
public let title: String

303
fi
public init(
id: UUID,
title: String
) {
self.id = id
self.title = title
}
}

struct Create: Codable {


public let title: String

public init(
title: String
) {
self.title = title
}
}

struct Update: Codable {


public let title: String

public init(
title: String
) {
self.title = title
}
}

struct Patch: Codable {


public let title: String?

public init(
title: String?
) {
self.title = title
}
}
}

The same thing applies for the public blog post DTOs.

// FILE: Sources/AppApi/Modules/Blog/BlogPost.swift

import Foundation

public extension Blog.Post {

struct List: Codable {


public let id: UUID
public let title: String
public let slug: String
public let image: String
public let excerpt: String
public let date: Date

public init(
id: UUID,
title: String,
slug: String,
image: String,
excerpt: String,
date: Date
) {
self.id = id
self.title = title
self.slug = slug
self.image = image
self.excerpt = excerpt
self.date = date
}

304
}

struct Detail: Codable {


public let id: UUID
public let title: String
public let slug: String
public let image: String
public let excerpt: String
public let date: Date
public let category: Blog.Category.List
public let content: String

public init(
id: UUID,
title: String,
slug: String,
image: String,
excerpt: String,
date: Date,
category: Blog.Category.List,
content: String
) {
self.id = id
self.title = title
self.slug = slug
self.image = image
self.excerpt = excerpt
self.date = date
self.category = category
self.content = content
}
}

struct Create: Codable {


public let title: String
public let slug: String
public let image: String
public let excerpt: String
public let date: Date
public let content: String
public let categoryId: UUID

public init(
title: String,
slug: String,
image: String,
excerpt: String,
date: Date,
content: String,
categoryId: UUID
) {
self.title = title
self.slug = slug
self.image = image
self.excerpt = excerpt
self.date = date
self.content = content
self.categoryId = categoryId
}
}

struct Update: Codable {


public let title: String
public let slug: String
public let image: String
public let excerpt: String
public let date: Date
public let content: String
public let categoryId: UUID

public init(
title: String,
slug: String,
image: String,

305
excerpt: String,
date: Date,
content: String,
categoryId: UUID
) {
self.title = title
self.slug = slug
self.image = image
self.excerpt = excerpt
self.date = date
self.content = content
self.categoryId = categoryId
}
}

struct Patch: Codable {


public let title: String?
public let slug: String?
public let image: String?
public let excerpt: String?
public let date: Date?
public let content: String?
public let categoryId: UUID?

public init(
title: String?,
slug: String?,
image: String?,
excerpt: String?,
date: Date?,
content: String?,
categoryId: UUID?
) {
self.title = title
self.slug = slug
self.image = image
self.excerpt = excerpt
self.date = date
self.content = content
self.categoryId = categoryId
}
}
}

Also, move the User namespace under the AppApi scope.

// FILE: Sources/AppApi/Modules/User/User.swift

public enum User: ApiModuleInterface {

public enum Account: ApiModelInterface {


public typealias Module = User
}

public enum Token: ApiModelInterface {


public typealias Module = User
}
}

Inside the User.Account object we're also going to de ne a brand new Login object; we're
going to use this later on when we perform a sign-in request.

// FILE: Sources/AppApi/Modules/User/UserAccount.swift

import Foundation

public extension User.Account {

struct Login: Codable {

306
fi
public let email: String
public let password: String

public init(
email: String,
password: String
) {
self.email = email
self.password = password
}
}

struct List: Codable {


public let id: UUID
public let email: String

public init(
id: UUID,
email: String
) {
self.id = id
self.email = email
}
}

struct Detail: Codable {


public let id: UUID
public let email: String

public init(
id: UUID,
email: String
) {
self.id = id
self.email = email
}
}

struct Create: Codable {


public let email: String
public let password: String

public init(
email: String,
password: String
) {
self.email = email
self.password = password
}
}

struct Update: Codable {


public let email: String
public let password: String?

public init(
email: String,
password: String
) {
self.email = email
self.password = password
}
}

struct Patch: Codable {


public let email: String?
public let password: String?

public init(
email: String?,
password: String?
) {
self.email = email
self.password = password

307
}
}
}

Finally, we have to make User.Token.Detail object publicly available.

// FILE: Sources/AppApi/Modules/User/UserToken.swift

import Foundation

public extension User.Token {

struct Detail: Codable {


public let id: UUID
public let value: String
public let user: User.Account.Detail

public init(
id: UUID,
value: String,
user: User.Account.Detail
) {
self.id = id
self.value = value
self.user = user
}
}
}

Now if you try to build and run the App target the compiler will complain about the missing
objects. One way to x this is to add the import AppApi line into each le, but there's a Swift
attribute that you can use to make the error disappear more easily.

The @_exported attribute starts with an underscore, this indicates that it hasn't gone through
the Swift evolution just yet, so it's not recommended to use; but still, many framework
authors take advantage of it.

This attribute imports the module for the entire local scope, so you don't have to import the
same module anywhere else if you mark it as exported.

// FILE: Sources/App/configure.swift

import Vapor
import Fluent
import FluentSQLiteDriver
import Liquid
import LiquidLocalDriver
@_exported import AppApi

public func configure(


_ app: Application
) throws {

// ...
}

Be careful with this private attribute, but it's good to know what it does. In this case, we're
going to x our issue by using it inside the con guration le.

308
fi
fi
fi
fi
fi
USING THE SHARED API LIBRARY
Now that we have a shared AppApi library, what else can we do with it? Of course! You can
create an iOS application and add the package as a dependency then link it with your target;
this way you don't have to de ne local DTOs, but you can reuse these objects straight from
the server-side.

This book is all about server-side Swift, so we won't create an iOS app, but we're going to
introduce a new test target and I'll show you how to make basic async network calls using
the new concurrent Foundation networking APIs.

// FILE: Package.swift

// swift-tools-version:5.7
import PackageDescription

let package = Package(


name: "myProject",
platforms: [
.macOS(.v12)
],
products: [
.library(name: "AppApi", targets: ["AppApi"]),
],
dependencies: [
.package(
url: "https://github.com/vapor/vapor",
from: "4.70.0"
),
.package(
url: "https://github.com/vapor/fluent",
from: "4.4.0"
),
.package(
url: "https://github.com/vapor/fluent-sqlite-driver",
from: "4.1.0"
),
.package(
url: "https://github.com/binarybirds/liquid",
from: "1.3.0"
),
.package(
url: "https://github.com/binarybirds/liquid-local-driver",
from: "1.3.0"
),
.package(
url: "https://github.com/binarybirds/swift-html",
from: "1.7.0"
),
.package(
url: "https://github.com/binarybirds/spec",
from: "1.2.0"
),
],
targets: [
.target(name: "AppApi", dependencies: []),
.target(name: "App", dependencies: [
.product(name: "Vapor", package: "vapor"),
.product(name: "Fluent", package: "fluent"),
.product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"),
.product(name: "Liquid", package: "liquid"),
.product(name: "LiquidLocalDriver", package: "liquid-local-driver"),
.product(name: "SwiftHtml", package: "swift-html"),
.product(name: "SwiftSvg", package: "swift-html"),
.target(name: "AppApi")
]),

309
fi
.executableTarget(name: "Run", dependencies: ["App"]),
.testTarget(name: "AppTests", dependencies: [
.target(name: "App"),
.product(name: "XCTVapor", package: "vapor"),
.product(name: "Spec", package: "spec"),
]),
.testTarget(name: "AppApiTests", dependencies: [
.target(name: "AppApi"),
]),
]
)

Since these tests aren't going to be simulated using a test environment, you should open a
command line window and start the web server by running the following command.

swift run Run

Now that the server is running, we can start writing some new tests. This test target only
relies on the XCTest framework, which auto-imports the Foundation framework, so we can
use the URLSession object to make network calls.

We're going to create an HTTPError enum, so we can throw a corresponding error message
if something goes wrong. The baseUrl is going to be de ned as a property: this way, we just
have to append the nal path component before we make our requests.

First, we need an authenticate method with the right user credentials. We're going to use the
User.Account.Login struct from the AppApi framework for this purpose.

We can construct an URLRequest object using the baseUrl with the sign-in path component.
We also have to set the method to POST and since we're going to submit the body as a
JSON value, we should always set the right Content-Type header.

The data(for:) method will return the HTTP response body and an URL response object. We
can try to cast the response into an HTTPURLResponse and check if the status code was in
the 200...299 range. If we've got a successful response we can cast the data into a proper
result type.

// FILE: Tests/AppApiTests/AppApiTests.swift

@testable import AppApi


import XCTest

final class AppApiTests: XCTestCase {

enum HTTPError: Error {


case invalidResponse
case invalidStatusCode(Int)
}

let baseUrl = URL(string: "http://localhost:8080/api/")!

private func authenticate(


_ login: User.Account.Login
) async throws -> User.Token.Detail {
var req = URLRequest(url: baseUrl.appendingPathComponent("sign-in/"))
req.httpMethod = "POST"
req.addValue("application/json", forHTTPHeaderField: "Content-Type")
req.httpBody = try JSONEncoder().encode(login)

let (data, response) = try await URLSession.shared.data(for: req)


guard let response = response as? HTTPURLResponse else {

310
fi
fi
throw HTTPError.invalidResponse
}
guard 200...299 ~= response.statusCode else {
throw HTTPError.invalidStatusCode(response.statusCode)
}
return try JSONDecoder().decode(User.Token.Detail.self, from: data)
}

private func authenticateRoot() async throws -> User.Token.Detail {


try await authenticate(
.init(
email: "[email protected]",
password: "ChangeMe1"
)
)
}

func testAuthorization() async throws {


let login = User.Account.Login(
email: "[email protected]",
password: "ChangeMe1"
)
let token = try await authenticate(login)
XCTAssertEqual(token.value.count, 64)
XCTAssertEqual(token.user.email, login.email)
}
}

We can also de ne a helper method to authenticate as a root user, and nally, we should
write a test method to check if the authorization process works or not. Of course, the server
has to run before you can execute these tests.

With this technique it's relatively simple to try out other endpoints as well, for example, we
can create a request to list all the blog category objects; this time we also have to provide
the authorization header before we execute the request.

// FILE: Tests/AppApiTests/AppApiTests.swift

@testable import AppApi


import XCTest

final class AppApiTests: XCTestCase {

enum HTTPError: Error {


case invalidResponse
case invalidStatusCode(Int)
}

let baseUrl = URL(string: "http://localhost:8080/api/")!

private func authenticate(


_ login: User.Account.Login
) async throws -> User.Token.Detail {
var req = URLRequest(url: baseUrl.appendingPathComponent("sign-in/"))
req.httpMethod = "POST"
req.addValue("application/json", forHTTPHeaderField: "Content-Type")
req.httpBody = try JSONEncoder().encode(login)

let (data, response) = try await URLSession.shared.data(for: req)


guard let response = response as? HTTPURLResponse else {
throw HTTPError.invalidResponse
}
guard 200...299 ~= response.statusCode else {
throw HTTPError.invalidStatusCode(response.statusCode)
}
return try JSONDecoder().decode(User.Token.Detail.self, from: data)
}

private func authenticateRoot() async throws -> User.Token.Detail {

311
fi
fi
try await authenticate(
.init(
email: "[email protected]",
password: "ChangeMe1"
)
)
}

func testAuthorization() async throws {


let login = User.Account.Login(
email: "[email protected]",
password: "ChangeMe1"
)
let token = try await authenticate(login)
XCTAssertEqual(token.value.count, 64)
XCTAssertEqual(token.user.email, login.email)
}

func testBlogCategories() async throws {


let token = try await authenticateRoot()

let path = "\(Blog.pathKey)/\(Blog.Category.pathKey)/"


var req = URLRequest(url: baseUrl.appendingPathComponent(path))
req.addValue("Bearer \(token.value)", forHTTPHeaderField: "Authorization")

let (data, response) = try await URLSession.shared.data(for: req)


guard let response = response as? HTTPURLResponse else {
throw HTTPError.invalidResponse
}

guard 200...299 ~= response.statusCode else {


throw HTTPError.invalidStatusCode(response.statusCode)
}
let decoder = JSONDecoder()
let categories = try decoder.decode([Blog.Category.List].self, from: data)
XCTAssertFalse(categories.isEmpty)
}
}

This is how you can easily perform network calls on iOS as well. You should note that the
new concurrent API supports cancellation too. With structured concurrency, you can wrap a
network call inside a Task and later on, it's possible to call the cancel method on the task
handler.

SUMMARY
This is how you can share DTOs between the backend and the frontend. Using a Swift
package to share these kinds of source snippets is a great way to take advantage of type-
safe Swift objects since you won't have duplicated code.

312
EPILOGUE

You might have these questions now:

- What's next?
- Where should I go to learn more?
- How should I build backend apps on my own?

The answer is quite simple, but, as always, it depends on your needs. I can only recommend
to join Vapor's discord server. You will see that the server side Swift community is amazing.
People are really friendly, helpful and open for discussions.

The o cial Vapor 4.0 documentation website is also a good place to nd out more about the
framework. Alternatively you can always check the sources on GitHub, I'm always looking at
the test les to know more about the internal parts of Vapor.

If you are looking for a Swift-based CMS solution, you should take a look at Feather CMS. It
is an open-source project, which is built on top of the same principles that I've showed you
in this book. Feel free to ask me anything about Feather or start a discussion on GitHub.

You should de nitely check my blog and subscribe to my newsletter. I write articles on a
weekly basis, mostly about server side Swift, but there are some iOS and other Swift
programming related posts as well. You can also reach me on Twitter or via email.

I hope you enjoyed reading my book.

Thank you for your support.

Tibor Bödecs

313
ffi
fi
fi
fi

You might also like