Winsights

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.

Tip Jar

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 Cloning

This would present you with 2 options to choose from for your project:

Use Fluent
  1. Use Fluent - An Object Relationship Mapper (ORM) to interact with a database. Input n.
Use Leaf
  1. Use Leaf - To embed response data in HTML templates for web browsers. Input y.
Generated FilesWelcome

Once done, run

cd TipCalculator && pwd

This will cd (change directory) to the new project directory and print your pwd (present working directory)

Path

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:

Folder Structure

At the root SPM

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

Resolved Packages

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 the executableTarget which contains the main.swift file serving as the launching point for the App.
  • App for the target 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
    • The routes file with routes implementation for registering routes. It has the root route and a hello route registered by default.

After the initial setup, click the Play button on the top left corner in XCode to build and run the server: Play

Your app builds, bringing up the server at the URL shown in the output console Start

Open Terminal and paste:

curl --location --request GET 'localhost:8080/hello'

(Like accessing localhost:8080/hello via a web browser)

Which will respond with

Hello

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

HTTP Primer

HTTP

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 pathHTTP method of communicationVapor response method for path
CreatePOSTpost
ReadGETget
UpdatePUTput
DeleteDELETEdelete

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 JSON

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. Stop

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: Response

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 Scheme

Then go to: Run -> Options -> Working Directory -> Use custom working directory 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: Page
On submitting the Amount and Tip Percentage you see the Tip and Total fields update as below:

Result

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.

Tagged with: