Getting Started With Vapor 4 — Part 1 | by Fernando Moya de Rivas | Mar, 2022

I must say I don’t have much experience in building web apps but as an iOS/macOS developer, I know Swift well. I’d heard of Vapor before but I had never dug into this framework. I must say that, after spending some time reading through their docs and trying it out, I’m delighted.

In this series of articles, I’ll explain some of the functionalities Vapor has to offer, and we’ll build a to-do app along the way. This article will mainly focus on giving a bit of context to “What is Vapor” and will cover some of the basics while getting our hands dirty with the framework.

You can find the full implementation in my repo:

https://github.com/fermoya/vapor-tutorial

What’s Vapor?

Vapor is a server-side framework written in Swift. It provides a set of tools to help you easily create a web app from scratch and/or define an API to support your mobile clients. We can find analogies in other well-known web application frameworks:

Why Vapor?

After some research, I found that Vapor was broadly accepted by the community. It’s a very mature framework that offers tons of possibilities, is easy to use, and is exactly what I was working for. With very little configuration, you can get it up and running in no time.

Plus, it’s written in Swift, which I’m familiar with, and can be used from Xcode and also from the terminal. Not only that, Vapor can be set up with both macOS and Linux or even create an image to run in Docker.

Is it the only server-side swift framework?

Not at all. Currently, there are other options. There’s Kitura and Perfect. However, neither of them is so widely accepted as Vapor.

What’s the best way of telling you what Vapor is if not by creating a sample app? Let’s run together a quick example to see how it works. We’ll be creating a to-do app and testing it with your terminal, Postman, or any other API client you might like.

Installation

If you don’t have it installed yet, go ahead and run brew install vapor . Make sure you have Homebrew installed and your Swift version is 5.2+.

Or, if you’re using Linux, follow the instructions here.

New project

To create a new project from a template, you just need to run vapor new <project_name> . By default, Vapor will ask if you’d like to use Leaf and/or Fluent (more on this later). Answer both with an . Alternatively, run the command with -n option:

$ vapor new TodoApp -n

Project structure

Let’s take a quick look at the project structure:

.
├── Dockerfile
├── Package.swift
├── Sources
│ ├── App
│ │ ├── Controllers
│ │ ├── configure.swift
│ │ └── routes.swift
│ └── Run
│ └── main.swift
├── Tests
│ └── AppTests
│ └── AppTests.swift
└── docker-compose.yml

There are three files you should familiarize yourself with:

  • main.swift . Here’s where your Application gets defined. You’ll probably want to leave this file mostly untouched.
  • configure.swift . This is the perfect spot to register services, configure a database, define some middleware or start your queue system among others. You can also change your HTTP configuration (hostname, port, …).
  • routes.swift . Use this file to define your endpoints and the routing of your app. In a real-life application you’ll be dealing with tens of endpoints (if not more) and, therefore, you’ll want to group related endpoints into Controllers .

Run the app

By default, Vapor will have created your project from a template so you should already have a couple of endpoints defined (you can double-check in your routes.swift file).

You have two options to run your app:

  • From Xcode: run vapor xcode inside the project folder and then ⌘R to run.
  • From the terminal: run vapor run serve

By default, your app runs in 127.0.0.1:8080 (or localhost:8080 ). You can easily test this with the following command:

$ curl localhost:8080/hello
Hello, world!%

At this point, the only thing that our app has to do with a to-do list is the project name. Let’s change that by letting an app client create a new to-do list.

Fluent and SQLite

Fluent is the supported ORM in Vapor. It officially supports SQLite, MySQL, PostgreSQL, and MongoDB. As our app is at an early stage, we’ll be using SQLite’s driver which supports in-memory database. In a real-life application, you’d want to use a persistent database, but for the sake of the sake of this tutorial, an in-memory database will do.

To configure Fluent and SQLite, go to your Package.swift and add both Fluent and FluentSQLiteDriver. It should look like this:

Leave the rest of the file unchanged. Finally, in your configure.swiftadd the following:

import Vapor
import Fluent
import FluentSQLiteDriver
public func configure(_ app: Application) throws {
app.databases.use(.sqlite(.memory), as: .sqlite)
try routes(app)
}

Models and migrations

A Model is an Entity that Fluent understands whereas a Migration is the code in charge of creating/updating your database tables. Somewhat like a record book or version control of all the changes made (and therefore, the order in which they’re added matters).

Let’s start with our Model and define how our TodoList will look like, as shown below:

A TodoList will contain, for now, just a name . Notice the use of @ID and @Field . The former indicates which field will be considered as the table lookup key. The latter is used to define the name of the table column. That is, a Model defines a database table and therefore it also defines its name by defining schema .

Once our model is defined, the only thing we need to do is create a Migration that will create the table, which is shown below:

Notice here how the migration will create a table holding two columns: an id and a name . Make sure the names are equivalent to those defined in your model. If this migration was to be reverted, the operation would be the opposite: the deletion of the table.

Finally, register your migration in your configure.swiftlike this:

// after registering your database...
app.migrations.add(TodoMigration(), to: .sqlite)
try app.autoMigrate().wait()

Note: Auto-migration is needed when we’re using an in-memory database.

Routes

At this point, we’re ready to start defining some endpoints. We’ll consider two operations:

  • GET: we’ll return all available TodoList
  • POST: we’ll allow a client to create a new TodoList

Endpoints are defined in the routes.swift . Open it and you’ll notice a func routes(_:) that takes an Application as an argument. This function is invoked from configure.swift and is used to define all routes allowed by your app.

Go ahead and place the following code inside:

app.get("todo-lists") { req in
TodoList.query(on: req.db)
.all()
.encodeResponse(for: req)
}

Here’s what this snippet is doing:

  • It defines a GET route/endpoint whose path is todo-lists
  • When invoked, we query the database to return all TodoList available
  • Finally, the array [TodoList] is returned as a response

Before continuing, you’ll have noticed a compiler error. This is because, in order to encode an entity as a JSON response to decode it from a request, the entity/model needs to conform to Content:

import Vaporextension TodoList: Content { }

Each new endpoint you define needs to return and EventLoopFuture. This is just a wrapper for an asynchronous operation that will run whenever your endpoint is called. Vapor is built on top of SwiftNIO.

Note: As of the latest Vapor versions, Vapor supports async/await . This means that the previously defined endpoint can be also expressed as the following:

app.get("todo-lists") { req in
try await TodoList
.query(on: req.db)
.all()
.get()
}

For the sake of compatibility, we’ll stick to the EventLoopFuture fashion, though.

Similarly, we can define a POST operation to create a TodoList:

app.post("todo-lists") { req in
try req.content
.decode(TodoList.self)
.save(on: req.db)
.transform(to: Response(status: .created))
}

In this case, we’re using post(_:use:) to define a POST operation in which we:

  • Try to decode a TodoList from the request content
  • We save it in the database
  • We transform the result to a HTTP 201 Created response

Let’s try it out:

$ curl localhost:8080/todo-lists
[]%
$ curl -X POST localhost:8080/todo-lists -H "Content-Type:application/json" --data "{ "name": "My first TODO list" }"
$ curl localhost:8080/todo-lists | jq
[
{
"id": "77D8FD15-D6C4-4EE1-9996-601370ED3329",
"name": "My first TODO list"
}
]

Nicely done, this is working like a charm! We’ve currently set up Create a Read operation. I’ll leave the implementation of Update and Delete as an exercise to complete all of CRUD. Hint: app defines a put (also a path) and a delete method.

By this point, you’ll hopefully have the opportunity to get a grasp of Vapor and are loving it as much as I do. In later articles, we’ll continue expanding the TodoApp and cover:

  • Advanced routing
  • Parameters and query parameters
  • Model relationships

Thanks for reading!

Good luck.

Leave a Comment