Getting Started With Vapor 3 Part 2:

Vapor App Structure

Posted 7/5/2018.

In Part 1 we saw how to install Vapor and create an Xcode project using the default api template. That project comes with a lot of code demonstrating the basics of building an API in Vapor. It uses SQLite as the database and a Todo model and controller as an example. Our app will eventually use PostgreSQL and will model information about U.S. national parks.

The first thing we'll do in this tutorial is clean up the template, removing all of the code we don't need and the SQLite dependency. We'll then go through the basic setup of a Vapor app.

Cleanup

We'll start by removing the unnecessary code and then we'll go through what's left in detail.

  1. Delete the Todo.swift and TodoController.swift files, but keep the Models and Controllers folders.

  2. Replace routes.swift with the following:

import Vapor

public func routes(_ router: Router) throws {
    router.get("hello", use: sayHelloWorld)
}

private func sayHelloWorld(_ request: Request) throws -> String {
    guard request.environment != .testing else {
        throw Abort(.badRequest, reason: "Route should not be called in Testing environment.")
    }
    return "Hello, world!"
}
  1. Replace configure.swift with the following:
import Vapor

public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws {
    let router = EngineRouter.default()
    try routes(router)
    services.register(router, as: Router.self)

    var middlewares = MiddlewareConfig()
    middlewares.use(ErrorMiddleware.self)
    services.register(middlewares)
}
  1. Replace Package.swift with the following:
// swift-tools-version:4.0
import PackageDescription

let package = Package(
    name: "NationalParks",
    dependencies: [
        .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0")
    ],
    targets: [
        .target(name: "App", dependencies: ["Vapor"]),
        .target(name: "Run", dependencies: ["App"]),
        .testTarget(name: "AppTests", dependencies: ["App"])
    ]
)

All that changed here is we removed the SQLite dependency.

  1. In Terminal, make sure you're in the root directory of the project. Then run vapor update to update the project based on the changes we just made to Package.swift. You will be prompted to regenerate and open the Xcode project. Enter y for at both prompts. You should see that the SQLite dependency has been removed.

  2. Finally, Vapor projects have many dependencies that we will never need to build ourselves, so it's helpful to remove their related schemes from the list we see in Xcode. Click on the scheme dropdown and go to "Manage Schemes". Uncheck all boxes except NationalParks-Package and Run. Now when you click on the schemes dropdown those are the only two you should see.

Basic App Structure

Vapor apps start in main.swift, which contains one line:

try app(.detect()).run()

All this does is call the global app function, which accepts one argument for the environment in which we're running the app (e.g. production, development, or testing). We'll see how to use environments when we run the app from the command line below.

Next look at app.swift, which defines the function called from main.swift.

public func app(_ env: Environment) throws -> Application {
    var config = Config.default()
    var env = env
    var services = Services.default()
    try configure(&config, &env, &services)
    let app = try Application(config: config, environment: env, services: services)
    try boot(app)
    return app
}

The Vapor framework includes a default configuration and set of services to use in our application. The app function uses the environment that was passed in and these defaults to configure and then initialize the application. Finally, we call the global boot function to do any post-initialization setup before returning the application instance. In the default api template, this boot function is empty.

Now let's look at configure.swift.

public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws {
    let router = EngineRouter.default()
    try routes(router)
    services.register(router, as: Router.self)

    var middlewares = MiddlewareConfig()
    middlewares.use(ErrorMiddleware.self)
    services.register(middlewares)
}

Here we do two things. First, we register our routes, which we'll see in a moment. Then we add and register middleware (more on the ErrorMiddleware below.

Finally, let's look at routes.swift.

public func routes(_ router: Router) throws {
    router.get("hello", use: sayHelloWorld)
}

private func sayHelloWorld(_ request: Request) throws -> String {
    guard request.environment != .testing else {
        throw Abort(.badRequest, reason: "Route should not be called in Testing environment.")
    }
    return "Hello, world!"
}

Here we have one route, a modified version of the /hello route from Part 1. This is a basic GET request, where the first argument is the path ("hello") and the second is a closure that takes a Request and returns a String.

In sayHelloWorld, we guard that the environment is not the testing environment. If it is, we throw an error ("Bad Request"). Otherwise, we return the string "Hello, world!".

If you run the app and go to http://localhost:8080/hello you should see "Hello, world!". This is because by default Vapor uses the "development" environment.

Environment & Middleware

To see how the app runs in the testing environment, we'll run it from the command line. In Terminal, make sure you're in the root directory of the project and run the following:

swift run Run --env test

This command runs the application using the scheme Run and passes in "test" for the environment argument. If you remember from main.swift, we passed in .detect() for the environment. To see the implementation of detect go to CommandInput.swift in Console > Command > Run and find the detect function in the Environment extension. You should see that the supported environment values are "prod", "dev", and "test".

When you run the above command, Terminal will report Server starting on http://localhost:8080. Now in your browser, refresh http://localhost:8080/hello. You should see the following JSON:

{"error":true,"reason":"Route should not be called in Testing environment."}

This is the error we threw in sayHelloWorld, so we know we've successfully run the app in the testing environment. But how is Vapor converting the error to the JSON we see in the browser?

That's where ErrorMiddleware comes in. Comment out the following line in configure.swift:

middlewares.use(ErrorMiddleware.self)

Now run the app from the command line again in the testing environment (swift run Run --env test).

To stop the previous run, enter control-C

If you refresh the /hello route in the browser, this time you'll get the browser's "Can't open page" error. This is because ErrorMiddleware is responsible for taking server errors and converting them into HTTP responses that can be viewed in the browser. Uncomment that line again, rerun the app from Terminal, and you should see the error again.


In this part, we saw how to clean up the default api template and went through the basic structure of a Vapor app. Next, we'll look at basic routing in Vapor.