A bidirectional Vapor router with more type safety and less fuss.

Overview

vapor-routing

A routing library for Vapor with a focus on type safety, composition, and URL generation.


Learn More

This library was discussed in an episode of Point-Free, a video series exploring functional programming and the Swift programming and the Swift language, hosted by Brandon Williams and Stephen Celis.

video poster image

Motivation

Routing in Vapor has a simple API that is similar to popular web frameworks in other languages, such as Ruby's Sinatra or Node's Express. It works well for simple routes, but complexity grows over time due to lack of type safety and the inability to generate correct URLs to pages on your site.

To see this, consider an endpoint to fetch a book that is associated with a particular user:

// GET /users/:userId/books/:bookId
app.get("users", ":userId", "books", ":bookId") { req -> BooksResponse in
  guard
    let userId = req.parameters.get("userId", Int.self),
    let bookId = req.parameters.get("bookId", Int.self)
  else {
    struct BadRequest: Error {}
    throw BadRequest()
  }

  // Logic for fetching user and book and constructing response...
  async let user = database.fetchUser(user.id)
  async let book = database.fetchBook(book.id)
  return BookResponse(...)
}

When a URL request is made to the server whose method and path matches the above pattern, the closure will be executed for handling that endpoint's logic.

Notice that we must sprinkle in validation code and error handling into the endpoint's logic in order to coerce the stringy parameter types into first class data types. This obscures the real logic of the endpoint, and any changes to the route's pattern must be kept in sync with the validation logic, such as if we rename the :userId or :bookId parameters.

In addition to these drawbacks, we often need to be able to generate valid URLs to various server endpoints. For example, suppose we wanted to generate an HTML page with a list of all the books for a user, including a link to each book. We have no choice but to manually interpolate a string to form the URL, or build our own ad hoc library of helper functions that do this string interpolation under the hood:

Node.ul(
  user.books.map { book in
    .li(
      .a(.href("/users/\(user.id)/book/\(book.id)"), book.title)
    )
  }
)
<ul>
  <li><a href="/users/42/book/321">Blob autobiography</a></li>
  <li><a href="/users/42/book/123">Life of Blob</a></li>
  <li><a href="/users/42/book/456">Blobbed around the world</a></li>
</ul>

It is our responsibility to make sure that this interpolated string matches exactly what was specified in the Vapor route. This can be tedious and error prone.

In fact, there is a typo in the above code. The URL constructed goes to "/book/:bookId", but really it should be "/books/:bookId":

- .a(.href("/users/\(user.id)/book/\(book.id)"), book.title)
+ .a(.href("/users/\(user.id)/books/\(book.id)"), book.title)

This library aims to solve these problems, and more, when dealing with routing in a Vapor application, by providing Vapor bindings to the URL Routing package.

Getting started

To use this library, one starts by constructing an enum that describes all the routes your website supports. For example, the book endpoint described above can be represented as a particular case:

enum SiteRoute {
  case userBook(userId: Int, bookId: Int)
  // more cases for each route
}

Then you construct a router, which is an object that is capable of parsing URL requests into SiteRoute values and printing SiteRoute values back into URL requests. Such routers can be built from various types the library vends, such as Path to match particular path components, Query to match particular query items, Body to decode request body data, and more:

import VaporRouting

let siteRouter = OneOf {
  // Maps the URL "/users/:userId/books/:bookId" to the
  // SiteRouter.userBook enum case.
  Route(.case(SiteRoute.userBook)) {
    Path { "users"; Digits(); "books"; Digits() }
  }

  // More uses of Route for each case in SiteRoute
}

Note: Routers are built on top of the Parsing library, which provides a general solution for parsing more nebulous data into first-class data types, like URL requests into your app's routes.

Once this little bit of upfront work is done, using the router doesn't look too dissimilar from using Vapor's native routing tools. First you mount the router to the application to take care of all routing responsibilities, and you do so by providing a closure that transforms SiteRoute to a response:

// configure.swift
public func configure(_ app: Application) throws {
  ...

  app.mount(siteRouter, use: siteHandler)
}

func siteHandler(
  request: Request,
  route: SiteRoute
) async throws -> any AsyncResponseEncodable {
  switch route {
  case let .userBook(userId: userId, bookId: bookId):
    async let user = database.fetchUser(user.id)
    async let book = database.fetchBook(book.id)
    return BookResponse(...)

  // more cases...
  }
}

Notice that handling the .userBook case is entirely focused on just the logic for the endpoint, not parsing and validating the parameters in the URL.

With that done you can now easily generate URLs to any part of your website using a type safe, concise API. For example, generating the list of book links now looks like this:

Node.ul(
  user.books.map { book in
    .li(
      .a(
        .href(siteRouter.path(for: .userBook(userId: user.id, bookId: book.id)),
        book.title
      )
    )
  }
)

Note there is no string interpolation or guessing what shape the path should be in. All of that is handled by the router. We only have to provide the data for the user and book ids, and the router takes care of the rest. If we make a change to the siteRouter, such as recognizing the singular form "/user/:userId/book/:bookId", then all paths will automatically be updated. We will not need to search the code base to replace "users" with "user" and "books" with "book".

Documentation

The documentation for releases and main are available here:

License

This library is released under the MIT license. See LICENSE for details.

Comments
  • Vapor Abort doesn't work as expected.

    Vapor Abort doesn't work as expected.

    I've replaced a part of the routes in an existing project using VaporRouting. I Replaced a single Vapour Controller by removing it from the registration in routes() and mounted a siteHandler and swift-parsing router that only covers the routes from that controller. Now within a site handler when throwing for example a .badRequest error for example by using: throw Abort(.badRequest, reason: "...") A response is returned with status .notFound, I'm expecting this to be the route not being found. However my router did reach the end it's just the siteHandler that failed somewhere down the chain. I would expect my response to return this .badRequest error

    opened by JaapWijnen 7
  • Unable to run with Docker

    Unable to run with Docker

    Hi! I am getting an error after adding this library to generated by vapor new project with fluent+postgre. This error does not appears with docker compose build but, after running docker compose up I got this:

    $ docker compose up   
    [+] Running 2/2
     โ ฟ Container testpointfree-db-1   Created                                                                                                         0.0s
     โ ฟ Container testpointfree-app-1  Recreated                                                                                                       0.1s
    Attaching to testpointfree-app-1, testpointfree-db-1
    testpointfree-db-1   | 
    testpointfree-db-1   | PostgreSQL Database directory appears to contain a database; Skipping initialization
    testpointfree-db-1   | 
    testpointfree-db-1   | 2022-05-24 13:57:41.910 UTC [1] LOG:  starting PostgreSQL 14.3 on x86_64-pc-linux-musl, compiled by gcc (Alpine 10.3.1_git20211027) 10.3.1 20211027, 64-bit
    testpointfree-db-1   | 2022-05-24 13:57:41.911 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
    testpointfree-db-1   | 2022-05-24 13:57:41.911 UTC [1] LOG:  listening on IPv6 address "::", port 5432
    testpointfree-db-1   | 2022-05-24 13:57:41.914 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
    testpointfree-db-1   | 2022-05-24 13:57:41.920 UTC [22] LOG:  database system was shut down at 2022-05-24 13:51:55 UTC
    testpointfree-db-1   | 2022-05-24 13:57:41.933 UTC [1] LOG:  database system is ready to accept connections
    testpointfree-app-1  | ./Run: error while loading shared libraries: libcurl.so.4: cannot open shared object file: No such file or directory
    testpointfree-app-1 exited with code 127
    

    My Package.swift:

    // swift-tools-version:5.6
    import PackageDescription
    
    let package = Package(
        name: "TestPointfree",
        platforms: [
           .macOS(.v12)
        ],
        dependencies: [
            // ๐Ÿ’ง A server-side Swift web framework.
            .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"),
            .package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"),
            .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0"),
            .package(url: "https://github.com/pointfreeco/vapor-routing", from: "0.1.0"),
        ],
        targets: [
            .target(
                name: "App",
                dependencies: [
                    .product(name: "Fluent", package: "fluent"),
                    .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"),
                    .product(name: "Vapor", package: "vapor"),
                    .product(name: "VaporRouting", package: "vapor-routing"),
                ],
                swiftSettings: [
                    // Enable better optimizations when building in Release configuration. Despite the use of
                    // the `.unsafeFlags` construct required by SwiftPM, this flag is recommended for Release
                    // builds. See <https://github.com/swift-server/guides/blob/main/docs/building.md#building-for-production> for details.
                    .unsafeFlags(["-cross-module-optimization"], .when(configuration: .release))
                ]
            ),
            .executableTarget(name: "Run", dependencies: [.target(name: "App")]),
            .testTarget(name: "AppTests", dependencies: [
                .target(name: "App"),
                .product(name: "XCTVapor", package: "vapor"),
            ])
        ]
    )
    

    Project without this library runs as expected.

    opened by ddanilyuk 2
  • Support ordered dictionaries from URL Routing 0.4.0

    Support ordered dictionaries from URL Routing 0.4.0

    We had one concrete instance of Dictionary that prevents Vapor Routing from compiling with URL Routing 0.4.0, unfortunately. Let's cut a quick release to fix.

    opened by stephencelis 1
  • ObjectId.parser() instand ->  UUID.parser()

    ObjectId.parser() instand -> UUID.parser()

    ObjectId is from mongo db

    public let wordsRouter = OneOf {
        Route(.case(WordsRoute.words)) {
            Path { "words2" }
        }
        
        Route(.case(WordsRoute.word)) {
            Path { "words2" }
            Path { Parse(.string) } // this how do it parse this like UUID.parser() *
            wordRouter
        }
    }
    
    func wordHandher(
        request: Request,
        wordID: String,
        route: WordRoute
    ) async throws -> AsyncResponseEncodable {
        switch route {
        case .fetch:
            guard let id = ObjectId.init(wordID) // * so I can remove this line 
            else {
                struct BadObjectId: Error {}
                throw BadObjectId()
            }
            
            return WordResponse(_id: id, englishWord: "english Word", englishDefinition: "English Definition", isReadFromNotification: false, isReadFromView: false, level: .beginner, url: router.url(for: .words(.word(id.hexString, .fetch))))
    }
    
    
    opened by addame2 1
  • Make middleware public

    Make middleware public

    Right now DocC does not document anything defined on types outside the module, which is currently the entirety of this module. Which makes rendered docs a bit awkward:

    image

    It doesn't hurt to publicize the middleware for better discoverability of documentation for now. In the future we should be able to safely make it private if we want to. It'll still be a bit threadbare, but not as bad.

    opened by stephencelis 0
  • How do I run on IOS for SiteRouter -> this target supports 9.0

    How do I run on IOS for SiteRouter -> this target supports 9.0

    I have create new SPM for my models so I can add server + iOS now when I add it iOS project I am getting this issue

    // swift-tools-version: 5.6
    // The swift-tools-version declares the minimum version of Swift required to build this package.
    
    import PackageDescription
    
    let package = Package(
        name: "WordNotificationSiteRouter",
        platforms: [.macOS(.v12)], -> I also try add `.iOS(.v14)`
        products: [
            // Products define the executables and libraries a package produces,
            // and make them visible to other packages.
            .library(name: "WordNotificationSiteRouter", targets: ["WordNotificationSiteRouter"]),
        ],
        dependencies: [
            .package(url: "https://github.com/pointfreeco/vapor-routing", from: "0.1.1"),
            .package(url: "https://github.com/pointfreeco/swift-parsing", from: "0.9.2"),
            .package(url: "https://github.com/vapor/fluent-mongo-driver.git", from: "1.1.2"),
        ],
        targets: [
            .target(
                name: "WordNotificationSiteRouter",
                dependencies: [
                    .product(name: "VaporRouting", package: "vapor-routing"),
                    .product(name: "_URLRouting", package: "swift-parsing"),
                    .product(name: "FluentMongoDriver", package: "fluent-mongo-driver"),
                ]),
            .testTarget(
                name: "WordNotificationSiteRouterTests",
                dependencies: ["WordNotificationSiteRouter"]),
        ]
    )
    

    Showing Recent Messages The package product 'FluentKit' requires minimum platform version 13.0 for the iOS platform, but this target supports 9.0 The package product 'MongoKitten' requires minimum platform version 12.0 for the iOS platform, but this target supports 9.0 The package product 'Vapor' requires minimum platform version 13.0 for the iOS platform, but this target supports 9.0

    opened by addame2 0
Releases(0.1.2)
Owner
Point-Free
A video series exploring Swift and functional programming.
Point-Free
Monarch Router is a Declarative URL- and state-based router written in Swift.

Monarch Router is a declarative routing handler that is capable of managing complex View Controllers hierarchy transitions automatically, decoupling View Controllers from each other via Coordinator and Presenters. It fits right in with Redux style state flow and reactive frameworks.

Eliah Snakin 31 May 19, 2021
Interface-oriented router for discovering modules, and injecting dependencies with protocol in Objective-C and Swift.

ZIKRouter An interface-oriented router for managing modules and injecting dependencies with protocol. The view router can perform all navigation types

Zuik 631 Dec 26, 2022
Crossroad is an URL router focused on handling Custom URL Scheme

Crossroad is an URL router focused on handling Custom URL Scheme. Using this, you can route multiple URL schemes and fetch arguments and parameters easily.

Kohki Miki 331 May 23, 2021
SwiftRouter - A URL Router for iOS, written in Swift

SwiftRouter A URL Router for iOS, written in Swift, inspired by HHRouter and JLRoutes. Installation SwiftRouter Version Swift Version Note Before 1.0.

Chester Liu 259 Apr 16, 2021
A demonstration to the approach of leaving view transition management to a router.

SwiftUI-RouterDemo This is a simplified demonstration to the approach of leaving view transition management to a router.

Elvis Shi 3 May 26, 2021
An experimental navigation router for SwiftUI

SwiftUIRouter ?? An โš ๏ธ experimental โš ๏ธ navigation router for SwiftUI Usage ?? Check out ExampleApp for more. Define your routes: import SwiftUIRouter

Orkhan Alikhanov 16 Aug 16, 2022
URLScheme router than supports auto creation of UIViewControllers for associated url parameters to allow creation of navigation stacks

IKRouter What does it do? Once you have made your UIViewControllers conform to Routable you can register them with the parameters that they represent

Ian Keen 94 Feb 28, 2022
An extremely lean implementation on the classic iOS router pattern.

Beeline is a very small library that aims to provide a lean, automatic implementation of the classic iOS router pattern.

Tim Oliver 9 Jul 25, 2022
Helm - A graph-based SwiftUI router

Helm is a declarative, graph-based routing library for SwiftUI. It fully describ

Valentin Radu 99 Dec 5, 2022
Provides a custom presentation modifier that provides more options including full screen presentations. (iOS)

Presentation Also available as a part of my SwiftUI+ Collection โ€“ just add it to Xcode 13+ Provides a custom presentation modifier that provides more

SwiftUI+ 15 Dec 3, 2022
๐Ÿ“ฑ๐Ÿ“ฒ Navigate between view controllers with ease. ๐Ÿ’ซ ๐Ÿ”œ More stable version (written in Swift 5) coming soon.

CoreNavigation ?? ?? Navigate between view controllers with ease. ?? ?? More stable version (written in Swift 5) coming soon. Getting Started API Refe

Aron Balog 69 Sep 21, 2022
Appz ๐Ÿ“ฑ Launch external apps, and deeplink, with ease using Swift!

Appz ?? Deeplinking to external applications made easy Highlights Web Fallback Support: In case the app can't open the external application, it will f

Kitz 1.1k May 5, 2021
๐ŸŽฏLinker Lightweight way to handle internal and external deeplinks in Swift for iOS

Linker Lightweight way to handle internal and external deeplinks in Swift for iOS. Installation Dependency Managers CocoaPods CocoaPods is a dependenc

Maksim Kurpa 128 May 20, 2021
๐Ÿž [Beta] A view controller that can unwind like presentation and navigation.

FluidPresentation - no more handling presented or pushed in view controller A view controller that supports the interactive dismissal by edge pan gest

Muukii 19 Dec 22, 2021
Easy and maintainable app navigation with path based routing for SwiftUI.

Easy and maintainable app navigation with path based routing for SwiftUI.

Freek Zijlmans 278 Jun 7, 2021
iOS routing done right. Handles both URL recognition and controller displaying with parsed parameters. All in one line, controller stack preserved automatically!

Developed and Maintained by Ipodishima Founder & CTO at Wasappli Inc. (If you need to develop an app, get in touch with our team!) So what is this lib

null 589 Dec 24, 2022
Eugene Kazaev 713 Dec 25, 2022
An open source library for building deep-linkable SwiftUI applications with composition, testing and ergonomics in mind

Composable Navigator An open source library for building deep-linkable SwiftUI applications with composition, testing and ergonomics in mind Vanilla S

Bahn-X 538 Dec 8, 2022
A framework for easily testing Push Notifications and Routing in XCUITests

Mussel ?? ?? A framework for easily testing Push Notifications, Universal Links and Routing in XCUITests. As of Xcode 11.4, users are able to test Pus

Compass 65 Dec 28, 2022