Skip to content

Instantly share code, notes, and snippets.

@djromero
Last active February 16, 2019 21:12
Show Gist options
  • Save djromero/51571ac884f384f9ce6dbb9830580ea2 to your computer and use it in GitHub Desktop.
Save djromero/51571ac884f384f9ce6dbb9830580ea2 to your computer and use it in GitHub Desktop.
First contact with Vapor

Vapor Notes

Notes taken while experimenting with Vapor (version 3.0, Feb 2019) using docker to run a vapor server and PostgreSQL.


Routes

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()])
}

Request parameters

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

Templates

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>

Database support

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.

PostgreSQL

To use PostgreSQL as ORM's backend:

Add dependencies

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.

Configure the database

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)

Define data model

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 { }

Queries

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

Migrations

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.

Middleware

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.

Development with Xcode

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)

In the server

Build

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

Run

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.

Docker

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.

PostgreSQL Installation and Tools

macOS

Download and install Postgres.app https://postgresapp.com/

Docker image

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

pgAdmin

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/

Install Logs

Vapor 3 Installation macOS

brew tap vapor/tap
brew install vapor/tap/vapor

Install Vapor 3 with swift 4.2.0 on Ubuntu

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

Install swift 4.2.1 on Ubuntu

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
@djromero
Copy link
Author

djromero commented Feb 9, 2019

Example of docker-compose.yml to run PostgreSQL and a vapor app in the same network:

version: '3'
services:
  db:
    image: "postgres"
    environment:
      - POSTGRES_DB=test
      - POSTGRES_USER=operator
      - POSTGRES_PASSWORD=0000
    networks: 
      - pgnet
  vapor:
    image: "vapor"
    ports:
      - "80:80"
    depends_on:
      - db
    environment:
      - DB_HOST=db
      - DB_USER=operator
      - DB_PASSWORD=0000
    networks: 
      - pgnet  
networks: 
  pgnet:
    driver: bridge

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment