Practical Server Side Swift 1.5.0
Practical Server Side Swift 1.5.0
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
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.
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.
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.
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.
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.
Preface 1
Chapter 1: Introduction 6
Summary 11
Installing Swift 12
Summary 24
Summary 52
Database migrations 57
Summary 69
Signing in 73
fi
Authenticators 77
Summary 83
Form components 89
Summary 96
Summary 108
Summary 123
List 131
Detail 135
Create 140
Update 144
Delete 146
Summary 148
Summary 184
Summary 195
List 197
Detail 199
Create 201
Update 203
Patch 205
Delete 207
Summary 230
Summary 260
Chapter 14: System Under Testing 261
Summary 282
Summary 300
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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.
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.
8
ffi
ff
fi
fl
ff
fi
fi
FEATURES
Vapor has
- user authentication
- web-socket support
- SQLite
- PostgreSQL
- MySQL
- MongoDB
SwiftHtml
- is a Swift library that can be used to generate both static and dynamic HTML
9
fi
- is checked by Swift's compiler for errors at compile time
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.
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
- 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.
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.
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.
# 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].
But, before we extract the archive, we have to install the required dependencies.
You can now extract the downloaded archive using the tar command. This also can take a
little time:
# example:
tar -xzf swift-5.7.2-RELEASE-amazonlinux2.tar.gz
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
swift --version
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
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:
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:
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:
Now you can list all the available Swift language versions:
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.
You can kick o a new Swift project by using SPM, so let's create a new myProject directory
and initialize a package.
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:
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.
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.
16
fi
fi
fi
ff
fi
fi
fi
fi
fi
fi
fi
fi
// swift-tools-version:5.7
import PackageDescription
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:
@main
public struct myProject {
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 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 line "[1] 9893" says that the app is running as background job 1, and the PID is 9893.
$ 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.
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:
You can nd the desired version list here. Alternatively, you can use the homebrew package
manager if you have macOS with homebrew installed.
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.
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.
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.
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.
21
fi
ff
fi
fi
fi
fi
fi
You can also generate a new Xcode project le based on the manifest by running:
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.
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
.
!"" 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.
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.
// swift-tools-version:5.7
import PackageDescription
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:
import Vapor
import SwiftHtml
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.
import Vapor
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.)
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
.
!"" 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:
import Vapor
import SwiftSgml
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.
import Vapor
import SwiftHtml
import Vapor
Now if we go back to our router le, we can create a new template and render it using the
req.templates extension.
29
fi
fi
fi
fi
fi
import Vapor
import SwiftHtml
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.
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.
import Vapor
import SwiftHtml
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.
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
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.
<!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.
import Vapor
import SwiftHtml
import SwiftSvg
extension Svg {
33
fi
.viewBox(minX: 0, minY: 0, width: 24, height: 24)
.fill("none")
.stroke("currentColor")
.strokeWidth(2)
.strokeLinecap("round")
.strokeLinejoin("round")
}
}
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 © 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 init(
title: String
) {
self.title = title
}
}
We have to move the message in the routes.swift le into the tag builder.
import Vapor
import SwiftHtml
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
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.
import Vapor
import SwiftHtml
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.
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.
38
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
import Vapor
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.
import Vapor
app.middleware.use(
FileMiddleware(
publicDirectory: app.directory.publicDirectory
)
)
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.
public init(
label: String,
39
fi
fi
fi
fi
fi
url: String
) {
self.label = label
self.url = url
}
}
import Vapor
import SwiftHtml
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.
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.
import Vapor
import SwiftHtml
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")
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.
import Vapor
struct WebFrontendController {
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.
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.
import Foundation
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.
import Vapor
struct BlogFrontendController {
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.
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.
import Vapor
import SwiftHtml
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.
import Vapor
struct BlogFrontendController {
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.
import Vapor
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.
import Vapor
app.middleware.use(
FileMiddleware(
publicDirectory: app.directory.publicDirectory
)
)
Run the application and navigate to the /blog/ page, you should see the list of posts.
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.
import Vapor
import SwiftHtml
init(
_ context: BlogPostContext
) {
self.context = context
}
@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"])
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.
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.
import Vapor
struct BlogFrontendController {
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.
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:
import Vapor
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.
50
ffi
fi
when we call it. You can read more about async / await and concurrency on the o cial Swift
website.
import Vapor
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.
import Vapor
app.middleware.use(
FileMiddleware(
publicDirectory: app.directory.publicDirectory
)
)
app.middleware.use(ExtendPathMiddleware())
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.
// swift-tools-version:5.7
import PackageDescription
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.
import Vapor
import Fluent
import FluentSQLiteDriver
// ...
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.
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:
import Vapor
import Fluent
struct FieldKeys {
struct v1 {
static var title: FieldKey { "title" }
}
}
init() { }
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.
import Vapor
import Fluent
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.
import Foundation
import Fluent
enum BlogMigrations {
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:
import Foundation
import Fluent
enum BlogMigrations {
// ...
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.
import Vapor
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.
import Vapor
60
fi
fi
ffi
fi
fi
fi
fi
fi
/// FILE: Sources/App/Modules/Blog/BlogModule.swift
import Vapor
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.
import Vapor
import Fluent
Now we have to update our models to support the newly created database model interface.
import Vapor
import Fluent
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.
import Vapor
import Fluent
// ...
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.
import Vapor
import Fluent
import FluentSQLiteDriver
app.middleware.use(
FileMiddleware(
publicDirectory: app.directory.publicDirectory
)
)
app.middleware.use(ExtendPathMiddleware())
We're ready to migrate the database and learn a bit more about Vapor commands.
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.
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.
import Vapor
import Fluent
import FluentSQLiteDriver
app.middleware.use(
FileMiddleware(
publicDirectory: app.directory.publicDirectory
)
)
app.middleware.use(ExtendPathMiddleware())
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.
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."
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
import Foundation
extension Blog.Post {
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.
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 {
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.
import Foundation
extension Blog.Post {
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.
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.
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.
import Vapor
import Fluent
struct BlogFrontendController {
return req.templates.renderHtml(
BlogPostsTemplate(ctx)
)
}
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
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.
import Vapor
import Fluent
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.
import Vapor
import Fluent
enum UserMigrations {
71
fi
.id()
.field(UserAccountModel.FieldKeys.v1.email, .string, .required)
.field(UserAccountModel.FieldKeys.v1.password, .string, .required)
.unique(on: UserAccountModel.FieldKeys.v1.email)
.create()
}
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.
import Vapor
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:
import Vapor
import Fluent
import FluentSQLiteDriver
// ...
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.
import Vapor
import Fluent
import FluentSQLiteDriver
73
fi
fi
fi
fi
fi
fi
// ...
app.sessions.use(.fluent)
app.migrations.add(SessionRecord.migration)
app.middleware.use(app.sessions.middleware)
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.
struct UserLoginContext {
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.
import Vapor
import SwiftHtml
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.
import Vapor
struct UserFrontendController {
76
ffi
fi
/// FILE: Sources/App/Modules/User/UserRouter.swift
import Vapor
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.
import Vapor
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.
import Vapor
public init(
id: UUID,
email: String
) {
self.id = id
self.email = email
}
}
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
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.
import Vapor
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.
import Vapor
struct UserFrontendController {
79
fi
private func renderSignInView(
_ req: Request,
_ input: Input? = nil,
_ error: String? = nil
) -> Response {
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.
import Vapor
import Fluent
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.
import Vapor
app.middleware.use(UserSessionAuthenticator())
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.
// ...
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.
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: "/")
}
}
import Vapor
func boot(
routes: RoutesBuilder
) throws {
routes.get("sign-in", use: frontendController.signInView)
routes
.grouped(
UserCredentialsAuthenticator()
)
.post("sign-in", use: frontendController.signInAction)
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.
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).
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.
import Vapor
import SwiftHtml
public init(
_ context: LabelContext
) {
self.context = context
}
@TagBuilder
public func render(
_ req: Request
) -> Tag {
Label {
Text(context.title ?? context.key.capitalized)
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.
import SwiftHtml
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.
import Vapor
import SwiftHtml
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.
import Vapor
86
fi
fi
fi
fi
fi
fi
> {
public init(
key: String,
input: Input,
output: Output,
error: String? = nil
) {
self.key = key
self.input = input
self.output = output
self.error = error
}
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.
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.
import SwiftHtml
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.
import Vapor
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.
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 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.
import Vapor
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.
import Vapor
public init(
key: String,
input: Input,
output: Output,
error: String? = nil
) {
self.key = key
self.input = input
self.output = output
self.error = error
}
90
fi
fi
fi
}
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.
import Vapor
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
}
91
fi
fi
}
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.
struct UserLoginContext {
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.
import Vapor
import SwiftHtml
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.
import Vapor
93
fi
fi
fi
fi
}
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.
import Vapor
struct UserFrontendController {
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.
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.
@resultBuilder
public enum FormComponentBuilder {
Now it's possible to mark the createFields method with the @FormComponentBuilder result
builder, and we can remove the bracket and column characters.
import Vapor
@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.
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.
import Vapor
97
fi
fi
fi
fi
fl
fl
private var saveBlock: FormFieldBlock?
public init(
key: String,
input: Input,
output: Output,
error: String? = nil
) {
self.key = key
self.input = input
self.output = output
self.error = error
}
// MARK: - FormComponent
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.
- load
- read
- render
- 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.
- 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
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.
import Vapor
public init(
key: String,
message: String
) {
self.key = key
self.message = message
}
}
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.
import Vapor
func validate(
_ req: Request
) async throws -> ValidationErrorDetail?
}
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.
import Vapor
public init(
abort: Abort,
message: String? = nil,
details: [ValidationErrorDetail]
) {
self.abort = abort
self.message = message
self.details = details
}
}
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.
import Vapor
public init(
_ validators: [AsyncValidator]
) {
self.validators = validators
}
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).
import Vapor
public init(
_ field: AbstractFormField<Input, Output>,
_ message: String,
_ validation: @escaping AsyncValidationBlock
) {
self.field = field
self.message = message
self.validation = validation
}
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.
import Vapor
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
}
}
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.
104
@resultBuilder
public enum AsyncValidatorBuilder {
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.
import Vapor
public init(
key: String,
input: Input,
output: Output,
error: String? = nil
) {
self.key = key
self.input = input
self.output = output
self.error = error
}
105
fi
fi
fi
fi
return self
}
// MARK: - FormComponent
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.
import Vapor
106
fi
fi
fi
fi
final class UserLoginForm: AbstractForm {
@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.
import Vapor
struct UserFrontendController {
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.
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.
import Vapor
import SwiftHtml
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.
import Vapor
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.
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.
import Vapor
import SwiftHtml
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)
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.
import Vapor
111
fi
fi
ff
fi
String,
TextareaFieldTemplate
> {
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.
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.
112
fi
fi
fi
// ...
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.
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.
import Vapor
import SwiftHtml
113
fi
public struct SelectFieldTemplate: TemplateRepresentable {
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)
The last step is to create the regular form eld class, this should look very familiar by now.
import Vapor
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.
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.
// swift-tools-version:5.7
import PackageDescription
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.
import Vapor
import Fluent
import FluentSQLiteDriver
import Liquid
import LiquidLocalDriver
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.
import Vapor
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.
import Vapor
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.
public init(
key: String,
name: String
) {
self.key = key
self.name = name
}
}
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.
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.
118
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
import Vapor
import SwiftHtml
public init(
_ context: ImageFieldContext
) {
self.context = context
}
@TagBuilder
public func render(
_ req: Request
) -> Tag {
LabelTemplate(context.label).render(req)
Input()
.type(.file)
.key(context.key)
.class("field")
.accept(context.accept)
Input()
.key(context.key + "TemporaryFileName")
.value(temporaryFile.name)
.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")
}
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.
import Vapor
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.
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 init(
_ key: String,
path: String
) {
self.path = path
super.init(
key: key,
input: .init(key: key),
output: .init(.init(key: key))
)
}
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
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.
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.
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
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 {
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.
import Vapor
import SwiftHtml
import SwiftSvg
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 © 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.
public init(
title: String
) {
self.title = title
}
}
Create a new AdminIndexTemplate inside the templates folder with the following contents:
import Vapor
import SwiftHtml
import SwiftSvg
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.
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.
import Vapor
import SwiftHtml
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.
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.
import Vapor
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.
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.
import Vapor
We'll have to register this new module inside the con guration le to make things work.
import Vapor
130
fi
fi
ffi
fi
fi
fi
import Fluent
import FluentSQLiteDriver
import Liquid
import LiquidLocalDriver
// ...
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.
struct BlogPostAdminListContext {
let title: String
let list: [Blog.Post.List]
}
import Vapor
import SwiftHtml
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.
import Vapor
struct BlogPostApiController {
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.
import Vapor
struct BlogPostAdminController {
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.
import Vapor
import Fluent
struct BlogFrontendController {
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.
import Vapor
133
fi
struct BlogRouter: RouteCollection {
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.
import Vapor
import SwiftHtml
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.
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.
import Vapor
import SwiftHtml
init(
_ context: BlogPostAdminDetailContext
) {
self.context = context
}
@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.
import Vapor
struct BlogPostApiController {
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.
import Vapor
import Fluent
struct BlogPostAdminController {
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.
import Vapor
import Fluent
struct BlogFrontendController {
137
fi
.sort(\.$date, .descending)
.all()
return req.templates.renderHtml(BlogPostsTemplate(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.
import Vapor
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.
import Vapor
import SwiftHtml
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.
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.
import Vapor
@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.
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.
import Vapor
import SwiftHtml
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.
import Vapor
import Fluent
struct BlogPostAdminController {
// ...
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.
import Vapor
posts.get(use: postAdminController.listView)
posts.get(":postId", use: postAdminController.detailView)
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.
import Vapor
import Fluent
struct BlogPostAdminController {
// ...
144
try await form.load(req: req)
try await form.read(req: req)
return renderEditForm(req, "Update post", form)
}
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.
import Vapor
posts.get(use: postAdminController.listView)
postId.get(use: postAdminController.detailView)
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.
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.
import Vapor
import SwiftHtml
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.
import Vapor
import Fluent
struct BlogPostAdminController {
// ...
As usual, we have to register the routes for these delete endpoint handlers.
import Vapor
147
ff
fi
.grouped(AuthenticatedUser.redirectMiddleware(path: "/"))
.grouped("admin", "blog", "posts")
posts.get(use: postAdminController.listView)
postId.get(use: postAdminController.detailView)
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.
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
import Vapor
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.
import Vapor
import SwiftHtml
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.
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.
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.
import Vapor
import SwiftHtml
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.
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.
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.
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.
import Vapor
import SwiftHtml
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.
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.
import Vapor
154
fi
fi
fi
fi
fi
fi
fi
import Fluent
init(
singular: String,
plural: String? = nil
) {
self.singular = singular
self.plural = plural ?? singular + "s"
}
}
func identifier(
_ req: Request
) throws -> UUID
func findBy(
_ id: UUID,
on: Database
) async throws -> DatabaseModel
}
extension ModelController {
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.
155
fi
public struct AdminListPageContext {
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.
import Vapor
import SwiftHtml
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.
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.
import Vapor
import SwiftHtml
import SwiftSvg
// ...
@TagBuilder
public func render(_ req: Request) -> Tag {
// ...
// .id(navigation)
Div {
Nav {
A("Admin")
.href("/admin/")
// 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.
import Vapor
func listContext(
_ req: Request,
_ list: [DatabaseModel]
) -> AdminListPageContext
func listTemplate(
_ req: Request,
_ list: [DatabaseModel]
) -> TemplateRepresentable
}
extension AdminListController {
158
fi
fi
fi
fi
fi
fi
[
LinkContext(label: "Create",path: "create")
]
}
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.
import Vapor
import Fluent
159
fi
fi
fi
fi
fi
]
}
// ...
}
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.
import Vapor
import Fluent
import Vapor
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.
import Vapor
import SwiftHtml
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.
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.
import Vapor
import SwiftHtml
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(" ")
}
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).
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
}
}
import Vapor
import SwiftHtml
163
fi
struct AdminDetailPageTemplate: TemplateRepresentable {
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.
import Vapor
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.
import Vapor
import Fluent
struct BlogCategoryAdminController:
AdminListController,
AdminDetailController
{
typealias DatabaseModel = BlogCategoryModel
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.
import Vapor
166
fi
struct BlogRouter: RouteCollection {
Don't forget to update the blog post controller and remove the unnecessary detailView
method.
import Vapor
import Fluent
struct BlogPostAdminController:
AdminListController,
AdminDetailController
{
typealias DatabaseModel = BlogPostModel
// ...
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.
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.
import Vapor
// ...
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.
import Vapor
@FormComponentBuilder
168
var formFields: [FormComponent] { get }
}
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.
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.
import Vapor
import SwiftHtml
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.
import Vapor
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 {
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.
import Vapor
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 {
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.
import Vapor
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.
import Vapor
import Fluent
struct BlogCategoryAdminController:
AdminListController,
AdminDetailController,
AdminCreateController,
AdminUpdateController
{
typealias DatabaseModel = BlogCategoryModel
typealias CreateModelEditor = BlogCategoryEditor
typealias UpdateModelEditor = BlogCategoryEditor
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.
import Vapor
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.
import Vapor
176
fi
fi
formatter.dateStyle = .long
formatter.timeStyle = .short
return formatter
}()
@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.
import Vapor
import Fluent
struct BlogPostAdminController:
AdminListController,
AdminDetailController,
AdminCreateController,
AdminUpdateController
{
typealias DatabaseModel = BlogPostModel
typealias CreateModelEditor = BlogPostEditor
typealias UpdateModelEditor = BlogPostEditor
178
fi
type: "post"
)
)
return req.templates.renderHtml(template)
}
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.
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.
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 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.
import Vapor
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)
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
}
import Vapor
import Fluent
struct BlogCategoryAdminController:
AdminListController,
AdminDetailController,
AdminCreateController,
AdminUpdateController,
AdminDeleteController
{
typealias DatabaseModel = BlogCategoryModel
typealias CreateModelEditor = BlogCategoryEditor
typealias UpdateModelEditor = BlogCategoryEditor
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
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.
import Vapor
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.
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.
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.
import Vapor
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.
import Vapor
186
ffi
ff
fi
fi
ffi
let categoryApiController = BlogCategoryApiController()
// ...
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.
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.
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.
import Foundation
extension Blog.Category {
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.
import Vapor
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.
import Vapor
// ...
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.
import Foundation
extension Blog.Category {
189
fi
fi
ff
fl
let id: UUID
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.
import Vapor
// ...
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.
import Vapor
190
fi
fi
let controller = BlogFrontendController()
let postAdminController = BlogPostAdminController()
let categoryAdminController = BlogCategoryAdminController()
let categoryApiController = BlogCategoryApiController()
// ...
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.
import Foundation
extension Blog.Category {
// ...
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.
import Vapor
// ...
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.
import Vapor
// ...
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.
import Foundation
extension Blog.Category {
// ...
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 (??).
import Vapor
// ...
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.
import Vapor
// ...
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.
import Vapor
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.
import Vapor
// ...
You can try the new endpoint by running this cURL snippet using the command line.
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.
import Vapor
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.
import Vapor
func listView(
_ req: Request
) async throws -> Response
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 {
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.
import Vapor
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.
import Vapor
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.
import Vapor
extension DetailController {
Let's add this conformance to the AdminDetailController; there's no need for other changes.
import Vapor
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.
import Vapor
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.
import Vapor
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.
import Vapor
extension CreateController {
import Vapor
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.
import Vapor
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.
import Vapor
struct BlogCategoryApiController:
ApiListController,
ApiDetailController,
ApiCreateController
{
typealias DatabaseModel = BlogCategoryModel
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.
import Vapor
import Vapor
// ...
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.
import Vapor
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
}
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.
import Vapor
struct BlogCategoryApiController:
ApiListController,
ApiDetailController,
ApiCreateController,
ApiUpdateController
{
typealias DatabaseModel = BlogCategoryModel
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.
import Vapor
import Vapor
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
}
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.
import Vapor
struct BlogCategoryApiController:
ApiListController,
ApiDetailController,
ApiCreateController,
ApiUpdateController,
ApiPatchController
{
typealias DatabaseModel = BlogCategoryModel
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.
import Vapor
import Vapor
init() {
207
super.init()
self.action.method = .post
self.submit = "Delete"
}
}
// ...
}
We can introduce one more API controller, the delete functionality can be moved there
without major changes.
import Vapor
func deleteApi(
_ req: Request
) async throws -> HTTPStatus
}
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.
import Vapor
struct BlogCategoryApiController:
ApiListController,
ApiDetailController,
ApiCreateController,
ApiUpdateController,
ApiPatchController,
ApiDeleteController
{
typealias DatabaseModel = BlogCategoryModel
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.
import Vapor
protocol AdminController:
AdminListController,
AdminDetailController,
AdminCreateController,
AdminUpdateController,
AdminDeleteController
{
extension AdminController {
import Vapor
import Fluent
// ...
}
import Vapor
import Fluent
// ...
}
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.
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.
import Vapor
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.
import Foundation
extension Blog.Post {
212
fi
let date: Date
let content: String
}
It's relatively easy to alter the original BlogPostApiController and use the new set of APIs.
import Vapor
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.
import Vapor
import Fluent
struct BlogFrontendController {
return req.templates.renderHtml(BlogPostsTemplate(ctx))
}
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.
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.
import Foundation
extension ApiModelInterface {
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.
import Vapor
import Fluent
215
fi
fi
fi
fi
public struct Name {
init(
singular: String,
plural: String? = nil
) {
self.singular = singular
self.plural = plural ?? singular + "s"
}
}
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(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.
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
),
]
}
// ...
}
import Vapor
// ...
extension AdminCreateController {
// ...
func createBreadcrumbs(
_ req: Request
) -> [LinkContext] {
[
LinkContext(
label: Self.moduleName.capitalized,
dropLast: 2
),
LinkContext(
label: Self.modelName.plural.capitalized,
dropLast: 1
),
]
}
}
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.
enum Blog {
}
}
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.
import Vapor
// ...
}
import Vapor
// ...
218
}
Because the ModelController has this new generic requirement, we also have to set the
ApiModel inside the admin controllers.
import Vapor
import Fluent
// ...
}
import Vapor
import Fluent
// ...
}
We're one step closer to a more reusable API layer that can be shared with other clients.
import Foundation
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.
import Vapor
extension ApiModelInterface {
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.
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.
import Vapor
import Fluent
init(
singular: String,
220
fi
plural: String? = nil
) {
self.singular = singular
self.plural = plural ?? singular + "s"
}
}
func identifier(
_ req: Request
) throws -> UUID
func findBy(
_ id: UUID,
on: Database
) async throws -> DatabaseModel
func getBaseRoutes(
_ routes: RoutesBuilder
) -> RoutesBuilder
}
extension ModelController {
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.
221
import Vapor
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.
import Vapor
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.
import Vapor
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)
}
}
import Vapor
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
)
}
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).
import Vapor
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.
import Vapor
func deleteApi(
_ req: Request
) async throws -> HTTPStatus
func setupDeleteRoutes(
_ routes: RoutesBuilder
)
}
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.
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.
import Vapor
// ...
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.
import Vapor
// ...
func setupDetailRoutes(
_ routes: RoutesBuilder
)
}
extension AdminDetailController {
// ...
func setupDetailRoutes(
_ routes: RoutesBuilder
) {
let baseRoutes = getBaseRoutes(routes)
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.
import Vapor
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.
import Vapor
extension AdminUpdateController {
// ...
func setupUpdateRoutes(
_ routes: RoutesBuilder
) {
let baseRoutes = getBaseRoutes(routes)
import Vapor
// ...
// ...
func setupDeleteRoutes(
_ routes: RoutesBuilder
)
}
extension AdminDeleteController {
// ...
func setupDeleteRoutes(
_ routes: RoutesBuilder
) {
let baseRoutes = getBaseRoutes(routes)
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.
import Vapor
postAdminController.setupRoutes(admin)
categoryAdminController.setupRoutes(admin)
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.
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.
import Vapor
import Fluent
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.
import Vapor
import Fluent
enum UserMigrations {
// ...
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.
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.
import Foundation
extension User.Account {
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.
import Foundation
extension User.Token {
233
let user: User.Account.Detail
}
}
Now we can focus on the controller that'll provide the logic for the API login handler.
import Vapor
struct UserApiController {
func signInApi(
req: Request
) async throws -> User.Token.Detail {
guard let user = req.auth.get(AuthenticatedUser.self) else {
throw Abort(.unauthorized)
}
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.
import Vapor
234
fi
fi
.post("sign-in", use: frontendController.signInAction)
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.
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.
import Vapor
import Fluent
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.
app.middleware.use(UserSessionAuthenticator())
app.middleware.use(UserTokenAuthenticator())
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.
import Vapor
236
fi
ff
.grouped("admin")
postAdminController.setupRoutes(admin)
categoryAdminController.setupRoutes(admin)
If you try to create a new category as a guest you should get an unauthorized response.
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
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.
import Vapor
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
}
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.
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
}
}
239
}
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.
import Vapor
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 {
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)
}
}
import Vapor
@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
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.
import Vapor
init(
message: String?,
details: [ValidationErrorDetail]
) {
self.message = message
self.details = details
}
}
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.
import Vapor
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)
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.
import Vapor
import Vapor
import Fluent
import FluentSQLiteDriver
import Liquid
import LiquidLocalDriver
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)
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
# {"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.
import Vapor
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
)
}
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)
}
}
import Vapor
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
}
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.
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] {
[]
}
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.
import Vapor
@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.
import Vapor
@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.
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.
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.
import Vapor
import Fluent
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.
import Vapor
import Fluent
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 {
func beforeDetail(
_ req: Request,
_ queryBuilder: QueryBuilder<DatabaseModel>
) async throws -> QueryBuilder<DatabaseModel> {
queryBuilder
}
func afterDetail(
_ req: Request,
_ model: DatabaseModel
) async throws -> DatabaseModel {
model
}
}
import Vapor
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.
import Vapor
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.
import Vapor
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 {}
}
import Vapor
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.
import Vapor
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.
import Vapor
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
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.
import Vapor
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.
import Vapor
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.
import Vapor
func patch(
_ req: Request,
_ model: DatabaseModel
) async throws
func beforePatch(
_ req: Request,
_ model: DatabaseModel
) async throws
func afterPatch(
_ req: Request,
_ model: DatabaseModel
) async throws
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.
import Vapor
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.
import Vapor
func delete(
_ req: Request,
_ model: DatabaseModel
) async throws
func beforeDelete(
_ req: Request,
_ model: DatabaseModel
) async throws
func afterDelete(
_ req: Request,
_ model: DatabaseModel
) async throws
}
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.
import Vapor
//...
// ...
}
extension AdminDeleteController {
//...
func deleteAction(
_ req: Request
) async throws -> Response {
let model = try await findBy(identifier(req), on: req.db)
try await delete(req, model)
//...
}
import Vapor
// ...
}
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.
import Vapor
import Fluent
// ...
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.
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.
261
fi
fi
fi
fl
fi
fi
swift test -l
swift test --list-tests
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.
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.
try configure(app)
app.databases.reinitialize()
app.databases.use(.sqlite(.memory), as: .sqlite)
app.databases.default(to: .sqlite)
try app.autoMigrate().wait()
return app
}
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.
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
}
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.
try configure(app)
app.databases.reinitialize()
app.databases.use(.sqlite(.memory), as: .sqlite)
app.databases.default(to: .sqlite)
try app.autoMigrate().wait()
return app
}
265
XCTAssertEqual(token.user.email, email)
}
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.
extension XCTApplicationTester {
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)
}
}
try configure(app)
app.databases.reinitialize()
app.databases.use(.sqlite(.memory), as: .sqlite)
app.databases.default(to: .sqlite)
try app.autoMigrate().wait()
return app
}
267
}
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)
}
}
}
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.
268
fi
@testable import App
import XCTVapor
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
)
}
}
import XCTVapor
extension XCTApplicationTester {
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.
Create a new BlogCategoryApiTests le for the blog category API-related test cases.
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
}
}
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.
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)
}
}
}
271
fl
fi
fi
let app = try createTestApp()
let token = try authenticateRoot(app)
defer { app.shutdown() }
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)
}
}
}
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.
// swift-tools-version:5.7
import PackageDescription
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.
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.
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()
}
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.
import Foundation
extension Blog.Post {
275
fi
let slug: String
let image: String
let excerpt: String
let date: Date
}
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.
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()
}
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.
import Vapor
@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.
import Vapor
@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.
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()
}
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()
}
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.
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.
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.
protocol HookFunction {
func invoke(_: HookArguments) -> Any
}
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.
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.
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.
284
fi
init() {
self.pointers = []
}
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.
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 {
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.
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.
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.
import Vapor
import SwiftHtml
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.
import Vapor
import SwiftHtml
@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.
import Vapor
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.
import Vapor
Inside the con guration le, we simply call the setUp method after the boot procedure.
import Vapor
import Fluent
import FluentSQLiteDriver
import Liquid
import LiquidLocalDriver
app.fileStorages.use(
.local(
publicUrl: "http://localhost:8080",
publicPath: app.directory.publicDirectory,
workDirectory: "assets"
),
as: .local
)
app.routes.defaultMaxBodySize = "10mb"
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)
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.
import Vapor
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.
import Vapor
We can now refactor the BlogRouter to take advantage of the new admin routes hook
system.
import Vapor
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.
import Vapor
291
struct BlogModule: ModuleInterface {
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.
import Vapor
We should alter the ApiModule le and call the setup method on the router.
import Vapor
292
fi
fi
/// FILE: Sources/App/Modules/Blog/BlogRouter.swift
import Vapor
postAdminController.setupRoutes(routes)
categoryAdminController.setupRoutes(routes)
}
postApiController.setupRoutes(routes)
categoryApiController.setupRoutes(routes)
}
}
As a very last step, we have to register our handler to listen for the "api-routes" event.
import Vapor
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.
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.
import Vapor
import Fluent
struct BlogFrontendController {
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.
import Vapor
postAdminController.setupRoutes(routes)
categoryAdminController.setupRoutes(routes)
}
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.
import Vapor
295
ff
fi
let router = BlogRouter()
func adminWidgetHook(
_ args: HookArguments
) -> TemplateRepresentable {
BlogAdminWidgetTemplate()
}
}
protocol AsyncHookFunction {
func invokeAsync(_: HookArguments) async throws -> Any
}
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.
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.
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.
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.
import Vapor
297
ffi
app.hooks.register("api-routes", use: router.apiRoutesHook)
app.hooks.registerAsync("response", use: router.responseHook)
func adminWidgetHook(
_ args: HookArguments
) -> TemplateRepresentable {
BlogAdminWidgetTemplate()
}
}
Just like we did before we should add our async helpers to the Application extension.
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.
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.
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
}
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.
import Vapor
299
The very last step in this chapter is to implement the setUp method inside the web module.
import Vapor
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.
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.
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
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"),
])
]
)
// FILE: Sources/AppApi/Framework/ApiModelInterface.swift
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)
}
}
// FILE: Sources/AppApi/Framework/ApiModuleInterface.swift
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
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 init(
id: UUID,
title: String
) {
self.id = id
self.title = title
}
}
303
fi
public init(
id: UUID,
title: String
) {
self.id = id
self.title = title
}
}
public init(
title: String
) {
self.title = title
}
}
public init(
title: String
) {
self.title = title
}
}
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 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
}
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
}
}
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
}
}
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
}
}
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
}
}
}
// FILE: Sources/AppApi/Modules/User/User.swift
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
306
fi
public let email: String
public let password: String
public init(
email: String,
password: String
) {
self.email = email
self.password = password
}
}
public init(
id: UUID,
email: String
) {
self.id = id
self.email = email
}
}
public init(
id: UUID,
email: String
) {
self.id = id
self.email = email
}
}
public init(
email: String,
password: String
) {
self.email = email
self.password = password
}
}
public init(
email: String,
password: String
) {
self.email = email
self.password = password
}
}
public init(
email: String?,
password: String?
) {
self.email = email
self.password = password
307
}
}
}
// FILE: Sources/AppApi/Modules/User/UserToken.swift
import Foundation
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
// ...
}
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
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.
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
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)
}
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
311
fi
fi
try await authenticate(
.init(
email: "[email protected]",
password: "ChangeMe1"
)
)
}
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
- 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.
Tibor Bödecs
313
ffi
fi
fi
fi