Starting with Vapor
Introduction
Waiters at hotels are getting creative with their tip jars and want customers to be able to tip as a percentage of their bill. They’ve approached you for a solution and you’ll not disappoint them.

In this tutorial you'll build a REST API and webpage that’ll Receive a bill amount with the tip percentage customers would like to give and Return the tip with the total amount using Swift (for its readability and speed) with the async focussed Vapor.
Requires XCode 13 or greater with Swift 5.5
Getting Started
Use Vapor Toolbox to get the basic folder structure and other scaffolding.
Open Terminal and install the toolbox using Homebrew with:
brew install vapor
Create the new project using
vapor new TipCalculator
This would present you with 2 options to choose from for your project:

- Use
Fluent
- An Object Relationship Mapper (ORM) to interact with a database. Input n.

- Use
Leaf
- To embed response data in HTML templates for web browsers. Input y.


Once done, run
cd TipCalculator && pwd
This will cd
(change directory) to the new project directory and print your pwd
(present working directory)

Note it for later use.
Open the project template in XCode using:
open Package.swift
XCode will download your project dependencies.
Navigating the Package and Folder Structure
In the Xcode Project navigator, you’ll see all the generated files:

At the root
Package.swift
:
Specifies the package targets (Source files compiled as a single unit to form a Module) with their dependencies. Replace the line // swift-tools-version:5.2
with // swift-tools-version:5.5
to use Swift 5.5 Replace .target(name: "Run"
with .executableTarget(name: "Run"
for compatibility

Package.resolve
:
Swift Package Manager
identifies and resolves the dependencies mentioned in the Package.swift
file along with any child dependencies (Dependency Management) to form a dependency graph with the most apt versions for each and records it here (Dependency Resolution)
Now if you run swift package show-dependencies
on the command line, it should tell you what it is pulling in transitively.
Sources folder
:
Contains one folder corresponding to every target defined in Package.swift
:
Run
for theexecutableTarget
which contains themain.swift
file serving as the launching point for theApp
.App
for thetarget
which has- the configure file with its configure function containing all the necessary configuration including
- Leaf usage with
app.views.use(.leaf)
- Call to register the routes
- Leaf usage with
- The routes file with routes implementation for registering routes. It has the root route and a hello route registered by default.
- the configure file with its configure function containing all the necessary configuration including
After the initial setup, click the Play button on the top left corner in XCode to build and run the server:
Your app builds, bringing up the server at the URL shown in the output console
Open Terminal and paste:
curl --location --request GET 'localhost:8080/hello'
(Like accessing localhost:8080/hello
via a web browser)
Which will respond with

Confirming the default hello GET
route is available on the loopback address.
HTTP Primer

When communicating between client and server applications there are four operations you need to perform, “Create”, “Read”, “Update” and “Delete” (CRUD) for which you have HTTP and Vapor methods as follows:
Client Request to path | HTTP method of communication | Vapor response method for path |
---|---|---|
Create | POST | post |
Read | GET | get |
Update | PUT | put |
Delete | DELETE | delete |
There are other methods like PATCH for partial updates.
Converting Content to and from JSON
Clients send request data in the HTTP method body (usually) in JSON. You’ll use Domain Transfer Objects (DTO) to convert to and from Swift structs by conforming them to Vapor’s Content
Protocol (an extension of the Codable
protocol). By default, Vapor uses JSONEncoder
and JSONDecoder
for encoding and decoding Content
Decoding
Assume you receive JSON data as below:
{
"amount": 150.5,
"tipPercentage": 5.5
}
Add this DTO to your routes file to map into Swift:
struct Bill: Content {
let amount: Decimal
let tipPercentage: Decimal
}
Then use request.content.decode( Bill.self )
to decode it and create a Bill struct with its data populated based on the JSON values.
Checking Content
Decoding from invalid JSON data will throw an error by default. To check Content values, add Validators to your routes file by conforming to Validatable
as so:
extension Bill: Validatable {
static func validations(_ validations: inout Validations) {
validations.add("amount", as: Decimal.self, is: .range(0.0...))
validations.add("tipPercentage", as: Decimal.self, is: .range(0.0...100.0))
}
}
Then call validate
before decode
like this:
try Bill.validate(content: req)
This checks if the tipPercentage
is between 0 and 100 and if the amount is positive
Encoding
Conversely, add this DTO to your routes file:
struct TotalBill: Content {
let amount: Decimal
let tipPercentage: Decimal
let tip: Decimal
let total: Decimal
}
Which encodes responses in JSON as:
{
"amount": 150.5,
"tipPercentage": 5.5,
"tip": 8.28,
"total": 158.78
}
Defining the Path
New Route
Add a route for the bill to the routes file:
app.post("bill", "total") { req throws -> TotalBill in
//Check JSON data
try Bill.validate(content: req)
//Decode JSON Payload from request body
let bill = try req.content.decode(Bill.self)
//Calculate the tip based on the inputs provided
let calculatedTip = bill.amount * bill.tipPercentage / 100.0
let calculatedTotal = bill.amount + calculatedTip
let total = TotalBill(amount: bill.amount, tipPercentage: bill.tipPercentage, tip: calculatedTip, total: calculatedTotal)
return total
}
This adds a new route to your app at localhost:8080/bill/total
which can receive a POST request with the amount and tipPercentage
in the JSON payload and respond to it with the tip and total in JSON.
Verify the Route
Stop your server if it is running.
Then build and run your app.
Open Terminal and paste this request:
curl --location --request POST 'localhost:8080/bill/total' \
--header 'Content-Type: application/json' \
--data-raw '{
"amount": 50.75,
"tipPercentage": 10.3
}'
To see JSON as:
Voila! You now have the API to calculate the tip and total amount so anyone can tip the server.
Tip - Serving files and static assets
Finding and serving files based on their relative path like those in the Resource and Public directories (including leaf templates) requires changing Xcode’s Working Directory to your project’s root directory. To do this Option + Click either the Scheme or Play button
Then go to: Run -> Options -> Working Directory -> Use custom working directory
And enter the Project directory noted earlier
Presenting the Path
The waiters also want the app to be accessible from browsers so you’ll wrap the response data within an HTML template to accommodate this.
###Enter Leaf
Add a file named bill.leaf
for the leaf template in Resources/Views folder as below:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Tip Calculator</title>
</head>
<body>
<form method="post">
<h3>Tip Calculator</h3>
<label for="amount">Amount: </label>
<input name="amount" type="number" min="0" step=".01" value="#(amount)" />
<label for="tipPercentage">Tip Percentage: </label>
<input name="tipPercentage" type="number" min="0" max="100" step=".01" value="#(tipPercentage)"/>
<b> Tip: #(tip) </b>
<b> Total: #(total) </b>
<button type="submit">
Calculate
</button>
</form>
</body>
</html>
The template contains static HTML with variables embedded within #(value)
which it replaces with data in the response.
Add two DTOs to the routes
file:
struct BillData: Encodable {
let amount: Double
let tipPercentage: Double
let tip: Double
let total: Double
}
BillData
for the initial view data and
struct BillForm: Decodable {
let amount: Double
let tipPercentage: Double
}
BillForm
to receive the form data
Finally add two routes to the routes
file:
app.get("bill") { req -> EventLoopFuture<View> in
let bill = BillData(amount: 0, tipPercentage: 15.0, tip: 0, total: 0)
return req.view.render("bill", bill)
}
Which renders the initial HTML page with default data using the bill
leaf template and
app.post("bill") { req -> EventLoopFuture<View> in
let billForm = try req.content.decode(BillForm.self)
let calculatedTip = billForm.amount * billForm.tipPercentage / 100.0
let calculatedTotal = billForm.amount + calculatedTip
let bill = BillData(amount: billForm.amount, tipPercentage: billForm.tipPercentage, tip: calculatedTip, total: calculatedTotal)
return req.view.render("bill", bill)
}
Which receives form data, calculates the tip and regenerates the view using the bill leaf template with the updated data.
Stop your server if it is running. Then build and run your app to see the initial page at http://localhost:8080/bill
on the browser with default values like:
On submitting the Amount and Tip Percentage you see the Tip and Total fields update as below:

The waiters have your praises on the tip of their tongues!
Where to Go From Here?
You’ve got a glimpse of how simple it is to build webpages and REST APIs with Vapor. The completed project is available for download here.
This is just the tip of the iceberg. To use a database, look into the Fluent ORM. Take it further with Async and the Event Loop in SwiftNIO to realise what’s happening under the hood. The official Vapor documentation and “Server-Side Swift with Vapor” book are great companion resources.
We give and take tips too. Leave yours in the forum below.