├── Config ├── secrets │ ├── .gitkeep │ └── app.json ├── production │ ├── app.json │ └── servers.json ├── app.json ├── development │ └── app.json └── servers.json ├── .swift-version ├── Procfile ├── .gitignore ├── Resources └── Views │ ├── template.leaf │ ├── embeds │ └── header.leaf │ └── welcome.html ├── Public ├── images │ └── vapor-logo.png └── styles │ └── app.css ├── Localization ├── en.json ├── es.json └── default.json ├── app.json ├── .travis.yml ├── Package.swift ├── Sources └── App │ ├── Middleware │ └── SampleMiddleware.swift │ ├── Models │ └── User.swift │ ├── Controllers │ └── UserController.swift │ └── main.swift ├── license └── README.md /Config/secrets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 3.0-GM-CANDIDATE 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: App --env=production --workdir="./" 2 | -------------------------------------------------------------------------------- /Config/secrets/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "secret-key" 3 | } -------------------------------------------------------------------------------- /Config/production/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "$VAPOR_APP_KEY" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Packages 2 | .build 3 | xcuserdata 4 | *.xcodeproj 5 | -------------------------------------------------------------------------------- /Config/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "default-key", 3 | "foo": "bar" 4 | } -------------------------------------------------------------------------------- /Config/development/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "development-key" 3 | } -------------------------------------------------------------------------------- /Config/production/servers.json: -------------------------------------------------------------------------------- 1 | { 2 | "production": { 3 | "port": "$PORT" 4 | } 5 | } -------------------------------------------------------------------------------- /Resources/Views/template.leaf: -------------------------------------------------------------------------------- 1 | #embed("embeds/header") 2 | 3 |

#(greeting)

4 | -------------------------------------------------------------------------------- /Public/images/vapor-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vapor-community/example/HEAD/Public/images/vapor-logo.png -------------------------------------------------------------------------------- /Config/servers.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": { 3 | "port": 8080, 4 | "host": "0.0.0.0", 5 | "securityLayer": "none" 6 | } 7 | } -------------------------------------------------------------------------------- /Localization/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome": { 3 | "title": "Welcome to Vapor!", 4 | "body": "A web framework for Swift." 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Resources/Views/embeds/header.leaf: -------------------------------------------------------------------------------- 1 | 2 | 7 | -------------------------------------------------------------------------------- /Localization/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome": { 3 | "title": "¡Bienvenidos a Vapor!", 4 | "body": "Un framework web de Swift." 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Localization/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome": { 3 | "title": "Default Welcome Message", 4 | "body": "Default welcome body." 5 | }, 6 | "other-key": "example" 7 | } 8 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vapor-example", 3 | "scripts": {}, 4 | "env": {}, 5 | "formation": {}, 6 | "addons": [], 7 | "buildpacks": [ 8 | { 9 | "url": "https://github.com/kylef/heroku-buildpack-swift" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - linux 3 | - osx 4 | language: generic 5 | sudo: required 6 | dist: trusty 7 | osx_image: xcode8 8 | install: 9 | - eval "$(curl -sL https://gist.githubusercontent.com/kylef/5c0475ff02b7c7671d2a/raw/02090c7ede5a637b76e6df1710e83cd0bbe7dcdf/swiftenv-install.sh)" 10 | script: 11 | - swift build 12 | - swift build -c release 13 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | import PackageDescription 2 | 3 | let package = Package( 4 | name: "VaporApp", 5 | dependencies: [ 6 | .Package(url: "https://github.com/vapor/vapor.git", majorVersion: 1, minor: 3) 7 | ], 8 | exclude: [ 9 | "Config", 10 | "Database", 11 | "Localization", 12 | "Public", 13 | "Resources", 14 | "Tests", 15 | ] 16 | ) 17 | -------------------------------------------------------------------------------- /Resources/Views/welcome.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Vapor 5 | 6 | 7 | 8 | 9 |
10 |

Vapor

11 | 12 | 21 |
22 | 23 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Sources/App/Middleware/SampleMiddleware.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | import HTTP 3 | 4 | class SampleMiddleware: Middleware { 5 | 6 | func respond(to request: Request, chainingTo chain: Responder) throws -> Response { 7 | // You can manipulate the request before calling the handler 8 | // and abort early if necessary, a good injection point for 9 | // handling auth. 10 | 11 | // return Response(status: .Forbidden, text: "Permission denied") 12 | 13 | let response = try chain.respond(to: request) 14 | 15 | // You can also manipulate the response to add headers 16 | // cookies, etc. 17 | 18 | return response 19 | 20 | // Vapor Middleware is based on S4 Middleware. 21 | // This means you can share it with any other project 22 | // that uses S4 Middleware. 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /Sources/App/Models/User.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | import Fluent 3 | 4 | final class User: Model { 5 | var id: Node? 6 | var name: String 7 | 8 | init(name: String) { 9 | self.name = name 10 | } 11 | 12 | init(node: Node, in context: Context) throws { 13 | id = try node.extract("id") 14 | name = try node.extract("name") 15 | } 16 | 17 | public func makeNode(context: Context) throws -> Node { 18 | return try Node(node: [ 19 | "name": name 20 | ]) 21 | } 22 | 23 | static func prepare(_ database: Database) throws { 24 | // 25 | } 26 | 27 | static func revert(_ database: Database) throws { 28 | // 29 | } 30 | } 31 | 32 | extension User { 33 | public convenience init?(from string: String) throws { 34 | self.init(name: string) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Tanner Nelson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Public/styles/app.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body, html { 6 | padding: 0; 7 | margin: 0; 8 | height: 100%; 9 | } 10 | 11 | body { 12 | font-family: sans-serif; 13 | color: #333; 14 | position: relative; 15 | } 16 | 17 | 18 | a { 19 | color: #92A8D1; 20 | text-decoration: none; 21 | border-bottom: 1px dotted; 22 | } 23 | 24 | a:hover { 25 | color: #F7CAC9; 26 | } 27 | 28 | div.wrapper { 29 | width: 100%; 30 | max-width: 600px; 31 | margin: 0 auto; 32 | position: relative; 33 | top: 50%; 34 | transform: translateY(-50%); 35 | margin-top: -25px; //adjust eye level 36 | } 37 | 38 | h1#logo { 39 | text-indent: -9999px; 40 | background-image: url(../images/vapor-logo.png); 41 | background-size: 100%; 42 | width: 347.5px; 43 | height: 84px; 44 | margin: 0 auto; 45 | } 46 | 47 | nav.main { 48 | text-align: center; 49 | margin-top: 20px; 50 | } 51 | nav.main ul { 52 | margin: 0; 53 | padding: 0; 54 | } 55 | nav.main ul li { 56 | display: inline-block; 57 | padding: 0 5px; 58 | } 59 | 60 | 61 | footer.main { 62 | position: absolute; 63 | width: 100%; 64 | text-align: center; 65 | bottom: 10px; 66 | } 67 | 68 | footer.main p { 69 | margin: 0; 70 | padding: 0; 71 | color: #ccc; 72 | font-weight: 300; 73 | } 74 | 75 | footer.main p a { 76 | color: #ccc; 77 | } -------------------------------------------------------------------------------- /Sources/App/Controllers/UserController.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | import HTTP 3 | 4 | final class UserController: ResourceRepresentable { 5 | typealias Item = User 6 | 7 | let drop: Droplet 8 | init(droplet: Droplet) { 9 | drop = droplet 10 | } 11 | 12 | func index(request: Request) throws -> ResponseRepresentable { 13 | return try JSON(node: [ 14 | "controller": "UserController.index" 15 | ]) 16 | } 17 | 18 | func store(request: Request) throws -> ResponseRepresentable { 19 | return try JSON(node: [ 20 | "controller": "UserController.store" 21 | ]) 22 | } 23 | 24 | /** 25 | Since item is of type User, 26 | only instances of user will be received 27 | */ 28 | func show(request: Request, item user: User) throws -> ResponseRepresentable { 29 | //User can be used like JSON with JsonRepresentable 30 | return try JSON(node: [ 31 | "controller": "UserController.show", 32 | "user": user 33 | ]) 34 | } 35 | 36 | func update(request: Request, item user: User) throws -> ResponseRepresentable { 37 | //User is JsonRepresentable 38 | return try user.makeJSON() 39 | } 40 | 41 | func destroy(request: Request, item user: User) throws -> ResponseRepresentable { 42 | //User is ResponseRepresentable by proxy of JsonRepresentable 43 | return user 44 | } 45 | 46 | func makeResource() -> Resource { 47 | return Resource( 48 | index: index, 49 | store: store, 50 | show: show, 51 | replace: update, 52 | destroy: destroy 53 | ) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vapor Example 2 | 3 | Fork this example project as a boilerplate for working with Vapor. 4 | 5 | Check out the [live demo](http://example.qutheory.io) running on Ubuntu. 6 | 7 | ## Badges 8 | [![Build Status](https://img.shields.io/travis/qutheory/vapor-example.svg?style=flat-square)](https://travis-ci.org/qutheory/vapor-example) 9 | [![PRs Welcome](https://img.shields.io/badge/prs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) 10 | [![Slack Status](http://slack.qutheory.io/badge.svg?style=flat-square)](http://slack.qutheory.io) 11 | 12 | ## Deploy 13 | 14 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) 15 | 16 | ## Documentation 17 | 18 | View [Vapor](https://github.com/qutheory/vapor) for documentation. 19 | 20 | ## Requirements 21 | 22 | Swift 3.0 preview 2 is required (Xcode 8 beta 2 on macOS). 23 | 24 | Works on Ubuntu, Docker, Heroku, macOS. 25 | 26 | Run the following script to check if you have Swift 3.0 beta 2 properly installed and configured. 27 | 28 | ``` 29 | curl -sL check.qutheory.io | bash 30 | ``` 31 | 32 | ## Building 33 | 34 | Visit [Getting Started](http://docs.qutheory.io) in the documentation. 35 | 36 | ### Compiling 37 | 38 | If you have the [Vapor Toolbox](https://github.com/qutheory/vapor-toolbox), use `vapor new ` to create your new application. 39 | 40 | Then run `vapor build` and `vapor run`. 41 | 42 | Otherwise, clone this repo and run `swift build` to compile your application, then run `.build/debug/App`. 43 | 44 | ### Xcode 8 45 | 46 | Run `vapor xcode` which will create the Xcode Project and open Xcode 8. 47 | 48 | ![Xcode](https://cloud.githubusercontent.com/assets/1342803/15592631/3e740df8-2373-11e6-8624-3c89260322aa.png) 49 | 50 | ## Deploying 51 | 52 | Check the [Vapor](https://github.com/qutheory/vapor) documentation for more in-depth deployment instructions. 53 | 54 | ### Upstart 55 | 56 | To start your `Vapor` site automatically when the server is booted, add this file to your server. 57 | 58 | You can check if Upstart is installed with 59 | 60 | ```sh 61 | initctl --version 62 | ``` 63 | 64 | You may need to install Upstart if it is not already on your installation of Linux. 65 | 66 | ```sh 67 | sudo apt-get install upstart 68 | ``` 69 | 70 | `/etc/init/vapor-example.conf` 71 | 72 | ```conf 73 | description "Vapor Example" 74 | 75 | start on startup 76 | 77 | env PORT=8080 78 | 79 | exec /home//vapor-example/.build/release/App --env=production 80 | ``` 81 | 82 | You additionally have access to the following commands for starting and stopping your server. 83 | 84 | ```shell 85 | sudo stop vapor-example 86 | sudo start vapor-example 87 | ``` 88 | 89 | The following script is useful for upgrading your website. 90 | 91 | ```shell 92 | git pull 93 | swift build --configuration release 94 | sudo stop vapor-example 95 | sudo start vapor-example 96 | ``` 97 | 98 | ### Heroku 99 | 100 | Use the `vapor heroku` commands in the Vapor Toolbox to push to Heroku. 101 | 102 | ### Docker 103 | 104 | You can run this demo application locally in a Linux environment using Docker. 105 | 106 | Make sure you have installed the Vapor Toolbox. 107 | 108 | 1. Ensure [Docker](https://www.docker.com) is installed on your local machine. 109 | 2. Start the Docker terminal 110 | 3. cd into `vapor-example` 111 | 4. Create the Dockerfile `vapor docker init` 112 | 5. Build the container `vapor docker build` 113 | 6. Run the container `vapor docker run` 114 | 7. Optionally enter the container `vapor docker enter` 115 | 5. Configure VirtualBox to [forward ports 8080 to 8080](https://www.virtualbox.org/manual/ch06.html) 116 | 6. Visit http://0.0.0.0:8080 117 | 118 | ### Nginx / Supervisor 119 | 120 | You can also run your Vapor app through Nginx. It’s recommended you use [Supervisor](http://supervisord.org) to run the app instance to protect against crashes and ensure it’s always running. 121 | 122 | #### Supervisor 123 | 124 | To setup Vapor running through Supervisor, follow these steps: 125 | 126 | `apt-get install -y supervisor` 127 | 128 | Edit the config below to match your environment and place it in `/etc/supervisor/conf.d/your-app.conf`: 129 | 130 | ```shell 131 | [program:your-app] 132 | command=/path/to/app/.build/release/App serve --ip=127.0.0.1 --port=8080 133 | directory=/path/to/app 134 | user=www-data 135 | stdout_logfile=/var/log/supervisor/%(program_name)-stdout.log 136 | stderr_logfile=/var/log/supervisor/%(program_name)-stderr.log 137 | ``` 138 | 139 | Now register the app with Supervisor and start it up: 140 | ```shell 141 | supervisorctl reread 142 | supervisorctl add your-app 143 | supervisorctl start your-app # `add` may have auto-started, so disregard an “already started” error here 144 | ``` 145 | 146 | #### Nginx 147 | 148 | With the app now running via Supervisor, you can use this sample nginx config to proxy it through Nginx: 149 | 150 | ```nginx 151 | server { 152 | server_name your.host; 153 | listen 80; 154 | 155 | root /path/to/app/Public; 156 | 157 | # Serve all public/static files via nginx and then fallback to Vapor for the rest 158 | try_files $uri @proxy; 159 | 160 | location @proxy { 161 | # Make sure the port here matches the port in your Supervisor config 162 | proxy_pass http://127.0.0.1:8080; 163 | proxy_pass_header Server; 164 | proxy_set_header Host $host; 165 | proxy_set_header X-Real-IP $remote_addr; 166 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 167 | proxy_connect_timeout 3s; 168 | proxy_read_timeout 10s; 169 | } 170 | } 171 | ``` 172 | -------------------------------------------------------------------------------- /Sources/App/main.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | import HTTP 3 | 4 | /** 5 | Droplets are service containers that make accessing 6 | all of Vapor's features easy. Just call 7 | `drop.serve()` to serve your application 8 | or `drop.client()` to create a client for 9 | request data from other servers. 10 | */ 11 | let drop = Droplet() 12 | 13 | /** 14 | Vapor configuration files are located 15 | in the root directory of the project 16 | under `/Config`. 17 | 18 | `.json` files in subfolders of Config 19 | override other JSON files based on the 20 | current server environment. 21 | 22 | Read the docs to learn more 23 | */ 24 | let _ = drop.config["app", "key"]?.string ?? "" 25 | 26 | /** 27 | This first route will return the welcome.html 28 | view to any request to the root directory of the website. 29 | 30 | Views referenced with `app.view` are by default assumed 31 | to live in /Resources/Views/ 32 | 33 | You can override the working directory by passing 34 | --workDir to the application upon execution. 35 | */ 36 | drop.get("/") { request in 37 | return try drop.view.make("welcome.html") 38 | } 39 | 40 | /** 41 | Return JSON requests easy by wrapping 42 | any JSON data type (String, Int, Dict, etc) 43 | in JSON() and returning it. 44 | 45 | Types can be made convertible to JSON by 46 | conforming to JsonRepresentable. The User 47 | model included in this example demonstrates this. 48 | 49 | By conforming to JsonRepresentable, you can pass 50 | the data structure into any JSON data as if it 51 | were a native JSON data type. 52 | */ 53 | drop.get("json") { request in 54 | return try JSON(node: [ 55 | "number": 123, 56 | "string": "test", 57 | "array": try JSON(node: [ 58 | 0, 1, 2, 3 59 | ]), 60 | "dict": try JSON(node: [ 61 | "name": "Vapor", 62 | "lang": "Swift" 63 | ]) 64 | ]) 65 | } 66 | 67 | /** 68 | This route shows how to access request 69 | data. POST to this route with either JSON 70 | or Form URL-Encoded data with a structure 71 | like: 72 | 73 | { 74 | "users" [ 75 | { 76 | "name": "Test" 77 | } 78 | ] 79 | } 80 | 81 | You can also access different types of 82 | request.data manually: 83 | 84 | - Query: request.data.query 85 | - JSON: request.data.json 86 | - Form URL-Encoded: request.data.formEncoded 87 | - MultiPart: request.data.multipart 88 | */ 89 | drop.get("data", Int.self) { request, int in 90 | return try JSON(node: [ 91 | "int": int, 92 | "name": request.data["name"]?.string ?? "no name" 93 | ]) 94 | } 95 | 96 | /** 97 | Here's an example of using type-safe routing to ensure 98 | only requests to "posts/" will be handled. 99 | 100 | String is the most general and will match any request 101 | to "posts/". To make your data structure 102 | work with type-safe routing, make it StringInitializable. 103 | 104 | The User model included in this example is StringInitializable. 105 | */ 106 | drop.get("posts", Int.self) { request, postId in 107 | return "Requesting post with ID \(postId)" 108 | } 109 | 110 | /** 111 | This will set up the appropriate GET, PUT, and POST 112 | routes for basic CRUD operations. Check out the 113 | UserController in App/Controllers to see more. 114 | 115 | Controllers are also type-safe, with their types being 116 | defined by which StringInitializable class they choose 117 | to receive as parameters to their functions. 118 | */ 119 | 120 | let users = UserController(droplet: drop) 121 | drop.resource("users", users) 122 | 123 | drop.get("leaf") { request in 124 | return try drop.view.make("template", [ 125 | "greeting": "Hello, world!" 126 | ]) 127 | } 128 | 129 | /** 130 | A custom validator definining what 131 | constitutes a valid name. Here it is 132 | defined as an alphanumeric string that 133 | is between 5 and 20 characters. 134 | */ 135 | class Name: ValidationSuite { 136 | static func validate(input value: String) throws { 137 | let evaluation = OnlyAlphanumeric.self 138 | && Count.min(5) 139 | && Count.max(20) 140 | 141 | try evaluation.validate(input: value) 142 | } 143 | } 144 | 145 | /** 146 | By using `Valid<>` properties, the 147 | employee class ensures only valid 148 | data will be stored. 149 | */ 150 | class Employee { 151 | var email: Valid 152 | var name: Valid 153 | 154 | init(request: Request) throws { 155 | email = try request.data["email"].validated() 156 | name = try request.data["name"].validated() 157 | } 158 | } 159 | 160 | /** 161 | Allows any instance of employee 162 | to be returned as Json 163 | */ 164 | extension Employee: JSONRepresentable { 165 | func makeJSON() throws -> JSON { 166 | return try JSON(node: [ 167 | "email": email.value, 168 | "name": name.value 169 | ]) 170 | } 171 | } 172 | 173 | // Temporarily unavailable 174 | //drop.any("validation") { request in 175 | // return try Employee(request: request) 176 | //} 177 | 178 | /** 179 | This simple plaintext response is useful 180 | when benchmarking Vapor. 181 | */ 182 | drop.get("plaintext") { request in 183 | return "Hello, World!" 184 | } 185 | 186 | /** 187 | Vapor automatically handles setting 188 | and retreiving sessions. Simply add data to 189 | the session variable and–if the user has cookies 190 | enabled–the data will persist with each request. 191 | */ 192 | drop.get("session") { request in 193 | let json = try JSON(node: [ 194 | "session.data": "\(request.session().data["name"])", 195 | "request.cookies": "\(request.cookies)", 196 | "instructions": "Refresh to see cookie and session get set." 197 | ]) 198 | var response = try Response(status: .ok, json: json) 199 | 200 | try request.session().data["name"] = "Vapor" 201 | response.cookies["test"] = "123" 202 | 203 | return response 204 | } 205 | 206 | /** 207 | Add Localization to your app by creating 208 | a `Localization` folder in the root of your 209 | project. 210 | 211 | /Localization 212 | |- en.json 213 | |- es.json 214 | |_ default.json 215 | 216 | The first parameter to `app.localization` is 217 | the language code. 218 | */ 219 | drop.get("localization", String.self) { request, lang in 220 | return try JSON(node: [ 221 | "title": drop.localization[lang, "welcome", "title"], 222 | "body": drop.localization[lang, "welcome", "body"] 223 | ]) 224 | } 225 | 226 | /** 227 | Middleware is a great place to filter 228 | and modifying incoming requests and outgoing responses. 229 | 230 | Check out the middleware in App/Middleware. 231 | 232 | You can also add middleware to a single route by 233 | calling the routes inside of `app.middleware(MiddlewareType) { 234 | app.get() { ... } 235 | }` 236 | */ 237 | drop.middleware.append(SampleMiddleware()) 238 | 239 | let port = drop.config["app", "port"]?.int ?? 80 240 | 241 | // Print what link to visit for default port 242 | drop.run() 243 | --------------------------------------------------------------------------------