Notes taken while experimenting with Vapor (version 3.0, Feb 2019) using docker to run a vapor server and PostgreSQL.
- Routes
- Request parameters
- Templates
- Database support
- PostgreSQL
- Add dependencies
- Configure the database
- Define data model
- Queries
- Migrations
- PostgreSQL
- Middleware
- Development with Xcode
- In the server
- Build
- Run
- Docker
- PostgreSQL Installation and Tools
- macOS
- Docker image
- pgAdmin
- Install Logs
- Vapor 3 Installation macOS
- Install Vapor 3 with swift 4.2.0 on Ubuntu
- Install swift 4.2.1 on Ubuntu
In routes.swift
add any additional route as needed.
You can use "inline" handlers:
router.get("new-route") { req in
return doStuffAndReturnString()
}
router.get("more", "stuff") { req in
return doMoreStuffAndReturnString()
}
router.get("article", Int.self) { req in
return articleById(...)
}
Any type conforming to Parameter
can be used.
Routes can be grouped:
let api = router.grouped("api")
api.get("v1", "list"){ ... }
api.get("v2", "list"){ ... }
For more complex routing needs it's better to create a RouteCollection
.
You can use a controller to group code related to a specific model:
let newController = NewController()
router.get("new-index", use: newController.index)
router.post("new-object", use: newController.create)
router.get("new-query", use: newController.lookup)
A controller is just any class. The defined functions' signatures depend on the expected returned data.
To return a list of model instances:
func f(_ req: Request) throws -> Future<[NewModel]> {
...
return NewModel.query(on: req).all()
}
To return a single instance:
func f(_ req: Request) throws -> Future<NewModel> {
...
return NewModel()
.create(on: req)
.save(on: req)
}
To return no instance:
func f(_ req: Request) throws -> Future<HTTPStatus> {
return try req.doSomething()
.doSomethingElse()
.transform(to: .ok)
}
To return a "view": (needs a template in Resources/Views/NewModelView.leaf
)
func f(_ req: Request) throws -> Future<View> {
return try req.view()
.render("NewModelView", ["items": NewModel.query(on: req).all()])
}
Get a parameter from the query string:
req.query[<#Type#>.self, at:"<#parameter-name#>"]
As in req.query[Int.self, at:"age"]
.
Decode a parameter from request's body:
req.parameters.next(<#Model#>.self)
As in req.parameters.next(NewModel.self)
.
Vapor support any template system, provided you write it first with its TemplateKit parser/renderer framework.
Out of the box there's Leaf.
Example: Resources/Views/NewModelView.leaf
<body>
<h1>All Items</h1>
<ul>
#for(item in items) {
<li><code>#(item.id)</code>: #(item.title)</li>
}
</ul>
</body>
Vapor uses ORM style access to the database with automatic migrations (à la rails or django). This means you don't define the database schema, you just create data models and let the ORM spawn the tables and map them to objects.
The ORM is called Fluent, it has support for the usual suspects: SQLite, MySQL and PostgreSQL.
I'm using PostgreSQL in my experimentation.
To use PostgreSQL as ORM's backend:
In Package.swift
:
Add PostgreSQL support via FluentPostgreSQL dependency:
.package(url: "https://github.com/vapor/fluent-postgresql.git", from: "1.0.0")
Run vapor xcode
.
In configure.swift
:
Register an instance of PostgreSQLDatabase
as database service:
var databases = DatabasesConfig()
try services.register(PostgreSQLProvider())
let postgresql = PostgreSQLDatabase(config: PostgreSQLDatabaseConfig(hostname: "<#host#>",
username: "<#user#>",
database: "<#ddbb#>",
password: "<#password#>"))
databases.add(database: postgresql, as: .psql)
services.register(databases)
Adjust the migration service:
var migrations = MigrationConfig()
migrations.add(model: NewModel.self, database: .psql)
services.register(migrations)
In NewModel.swift
:
Models must derive from PostgreSQLModel
:
import FluentPostgreSQL
final class NewModel: PostgreSQLModel
{
/// The unique identifier
var id: Int?
...
var age: Int
var whatever: OtherCodableType
}
Keep id
definition as Optional<Int>
to play nicely with PostgresQL SERIAL (aka autoincrement) columns.
Make it conform to Migration
, Content
and Parameter
protocols to have automatic schemas, object encoding/decoding, and dynamic parameters in routes (equivalent to /path/:property
kind of routes in other frameworks).
extension NewModel: Migration { }
extension NewModel: Content { }
extension NewModel: Parameter { }
Use the incoming request to access the database unless you need something fancy. The request is a container that has the database services as dependencies.
find
to get a single instance by identifier (e.g.NewModel.find(99, on: req)
)query
to read instances:all
fetchs everything (e.g.NewModel.query(on: req).all()
)filter
selects using a predicate (e.g.NewModel.query(on: req).filter(\.age == 42)
)
create
to add new model instances (e.g.NewModel().create(on: req)
)save
to commit (e.g.NewModel.save(on: req)
)
Fluent is a full ORM, you can also perform joins on related models, sorting, complex predicates, limit results...
Beyond the migration (just by conforming to Migration
) to create initial schema for basic models, you'll need to create migrations manually (like in any other framework).
For example, to add new field to a model:
import FluentPostgreSQL
struct AddEyesColor: PostgreSQLMigration
{
static func prepare(on conn: PostgreSQLConnection) -> Future<Void> {
return PostgreSQLDatabase.update(Person.self, on: conn) { builder in
builder.field(for: \.eyesColor)
}
}
static func revert(on conn: PostgreSQLConnection) -> Future<Void> {
return PostgreSQLDatabase.update(Person.self, on: conn) { builder in
builder.deleteField(for: \.eyesColor)
}
}
}
All changes must have a revert method to undo the modifications.
Each migration should be added at configuration time:
var migrations = MigrationConfig()
...
migrations.add(migration: DelEyesNumber.self, database: .psql)
migrations.add(migration: AddEyesColor.self, database: .psql)
services.register(migrations)
Depending on the performed alterations, the order is important. Fluent keeps track of applied migrations in a helper table.
Modify incoming requests and outgoing responses; or add side effects.
Any class that conform to Middleware
. Example:
final class LogMiddleware: Middleware, Service
{
let log: Logger
init(log: Logger) {
self.log = log
}
func respond(to request: Request, chainingTo next: Responder) throws -> Future<Response> {
log.verbose("[\(Date())] \(request.http.method) \(request.http.url.path)")
return try next.respond(to: request)
}
}
In configure.swift
:
var middlewares = MiddlewareConfig()
...
middlewares.use(LogMiddleware.self) // custom request logging
services.register(middlewares)
Calls to middelware services are chained. Order matters.
Edit Package.swift
, then:
vapor xcode
The generated Xcode project should be considered temporary. You don't need even need to add it to your source code repository.
Open the generated .xcodeproj
and run/debug as usual.
- Add files in the designated folders (Controllers, Models, Resources, Middleware, Services...check the docs)
- I don't know how to deal with arguments and other flags (for now I recreate them)
To build the project you need swift, vapor and your sources.
- Add Vapor APT repo
eval "$(curl -sL https://apt.vapor.sh)"
- Install swift and vapor
apt-get install -y swift vapor
- Download your source code
- Build the project
cd /path/to/project
vapor build
To run the project you just need swift and your project binary.
- Install swift (using Vapor APT or read below to install manually)
- Copy the app binary (named
Run
inside the.build
directory) to your deployment server, put it in some directory in PATH. Rename it, maybe.
Run serve --env production --hostname 0.0.0.0 --port 80
Depending on the distribution you may need to install libcurl4, libicu-dev, libxml2, libbsd0, libatomic1, tzdata.
Vapor creates a Dockerfile with the project. Just build and deploy it.
To build, go to your project directory:
docker build -f <Dockerfile> --build-arg env=docker -t vapor .
It's tagged as "vapor".
To run locally:
docker run —name vapor -ti -p 80:80 vapor
Add --network
argument as required.
Download and install Postgres.app https://postgresapp.com/
Official docker image from https://hub.docker.com/_/postgres
Create a network to share with other containers:
docker network create --driver bridge pg-net
Then run:
docker run --name psql --network pg-net --p 5432:5432 \
-v ${HOME}/Documents/pgdata:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD='<password>' -d postgres
PostgreSQL browser and admin tool.
Run in docker from https://hub.docker.com/r/dpage/pgadmin4
docker run --name pgadmin --network pg-net -ti -d -p 4040:80 \
-e PGADMIN_DEFAULT_EMAIL='<email>' \
-e PGADMIN_DEFAULT_PASSWORD='<password>' -d dpage/pgadmin4
Browse http://localhost:4040/
brew tap vapor/tap
brew install vapor/tap/vapor
As root
:
eval "$(curl -sL https://apt.vapor.sh)"
echo "deb https://repo.vapor.codes/apt $(lsb_release -sc) main" | tee /etc/apt/sources.list.d/vapor.list
apt-get install vapor
As root
:
apt-get install -y clang libicu-dev wget
wget https://swift.org/builds/swift-4.2.1-release/ubuntu1804/swift-4.2.1-RELEASE/swift-4.2.1-RELEASE-ubuntu18.04.tar.gz
tar xzf *.tar.gz
rm *.tar.gz
mv swift-4.2.1-RELEASE-ubuntu18.04 /usr/share/swift
export PATH=/usr/share/swift/usr/bin:"${PATH}"
swift —version
Example of
docker-compose.yml
to run PostgreSQL and a vapor app in the same network: