-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Optionals
-
Section 4: Object-Oriented and Value Types
-
Section 5: Memory Management
-
Section 6: Generics and Error Handling
-
Section 7: Concurrency
-
Section 8: Working with Collections
-
Section 9: Codable and Data Handling
-
Section 10: Protocol-Oriented Programming
-
Section 11: Testing and Tooling
-
Section 12: Practical Projects
-
Section 13: Interview Practice
-
Section 14: More Practice Exercises
-
Section 15: More Standard Library
-
Section 16: Advanced Concurrency
-
Section 17: Foundation Framework Deep Dive
-
Section 18: URLSession and Networking Deep Dive
-
Section 19: Combine Framework
-
Section 20: SwiftUI Fundamentals for Swift Developers
-
Section 21: Server-Side Swift with Vapor
-
Section 22: Swift Package Manager Deep Dive
-
Section 23: Swift Concurrency Deep Dive
-
Section 24: More Language Features
-
Section 25: Error Handling Deep Dive
-
Section 26: Testing Deep Dive
-
Section 27: Data Structures and Algorithms in Swift
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Swift Macros (Swift 5.9+)
-
Section 31: Property Wrappers Ecosystem
-
Section 32: Swift Interop Deep Dive
-
Section 33: iOS App Architecture Patterns
-
Section 34: Performance and Debugging
-
Section 35: App Distribution and CI/CD
-
Section 36: More Practical Projects
-
Section 37: SwiftData and Persistence
-
Section 38: More Design Patterns
-
Section 39: More Review and Practice
-
Section 40: More Foundation Deep Dive
-
Section 41: Advanced Collections in Swift
-
Section 42: Advanced Generics Practice
-
Section 43: UIKit for Legacy and Hybrid Apps
-
Section 44: watchOS and visionOS Development Basics
-
Section 45: More Networking Patterns
-
Section 46: More Testing Practice
-
Section 47: Accessibility in Swift Apps
-
Section 48: Localization
-
Section 49: More Practical Projects Round 2
-
Section 50: Swift Charts Framework
-
Section 51: More Interview and Algorithm Practice
-
Section 52: Final Practice and Mastery
-
Section 53: Swift Compiler and Build System
-
Section 54: More Concurrency Practice
-
Section 55: App Store Guidelines and Review
-
Section 56: More Design and Architecture
55: Building a Simple Server-Side Swift API with Vapor
Moving from iOS development to server-side Swift with Vapor can feel like stepping into a mirror dimension. You're using the same language, but the environment is entirely different. There's no UIKit, no SwiftUI, and instead of managing a view hierarchy, you're managing a request-response lifecycle. I've seen a lot of developers treat their first Vapor project like a script, throwing everything into the routing file because it's the fastest way to see a result in the browser.
The Temptation of the Giant Route File
When you first set up a project, Vapor gives you a routes.swift file. It's tempting to just start writing your logic right there. Let's say we're building a simple Book Inventory API. The naive approach is to handle the database query, the validation, and the JSON response all inside a single closure.
app.get("books", ":bookID") { req -> EventLoopFuture<Book> in
let bookID = req.parameters.get("bookID", as: UUID.self)
// Imagine doing validation, logging, and DB queries all here...
return Book.find(bookID, on: req.db)
.unwrap(or: Abort(.notFound))
}
At first, this feels efficient. You have one place to look, and it's only a few lines of code. But here is where it falls apart: the moment you add a second or third endpoint—like adding a book or updating a price—your routes.swift becomes a "God file." It grows into a thousand-line monster where business logic is tangled up with routing definitions. If you want to write a unit test for the logic of "how we find a book," you can't, because that logic is trapped inside a route handler that requires a full Request object to function.
Moving Toward Controller-Based Logic
The better way, and the way we actually do this in production, is to decouple the routing (the "where" of the request) from the controller (the "what" of the request). I think of the route file as a switchboard operator; its only job is to take the call and plug it into the right department.
Instead of an anonymous closure, we create a BookController. This allows us to group related functionality and, more importantly, keep our routing table clean. Here is how that looks in practice:
struct BookController: RouteCollection {
func boot(routes: RoutesBuilder) throws {
let books = routes.grouped("books")
books.get(":bookID", use: find),
books.post(use: create)
}
func find(req: Request) throws -> EventLoopFuture<Book> {
guard let bookID = req.parameters.get("bookID", as: UUID.self) else {
throw Abort(.badRequest)
}
return Book.find(bookID, on: req.db).unwrap(or: Abort(.notFound))
}
func create(req: Request) throws -> EventLoopFuture<Book> {
let book = try req.content.decode(Book.self)
return book.save(on: req.db).map { book }
}
}
Now, in your routes.swift, you simply call try app.register(collection: BookController()). You've just moved the "mess" into a dedicated space where it can be managed.
The Trade-off: Boilerplate vs. Maintainability
I'll be honest with you: this approach requires more files and more typing upfront. You have to define a struct, implement RouteCollection, and explicitly map your methods. When you're building a prototype in an hour, that feels like overkill.
But consider the "break point." The naive approach breaks the moment you need to change your database schema or add complex validation logic. If you have 20 routes all living in one file, a single syntax error can stop the entire server from compiling, and finding the culprit becomes a game of hide-and-seek. By using controllers, you isolate the failure. If the BookController has a bug, your UserController is still perfectly fine. It also makes the code much more readable for your teammates; they can look at the folder structure and know exactly where the book-related logic lives without scrolling through a 2,000-line file.
One last tip: notice the use of req.content.decode(Book.self). In the naive approach, people often try to manually parse JSON dictionaries. Never do that in Vapor. Leverage the Content protocol. It handles the heavy lifting of serialization and deserialization, ensuring that if the client sends a malformed JSON body, Vapor automatically throws a 400 Bad Request before the code even hits your controller logic.
📋 Practical Task
Exercise: Implementing a Validated Book Archive Endpoint
You are tasked with adding a new feature to the Book Inventory API. You need to create a specialized endpoint that archives a book, but only if the book exists and is not already archived.
Requirements:
- Do not add this logic to
routes.swift. Instead, add a new method to theBookController. - Create a route:
PATCH /books/:bookID/archive. - The logic should:
- Fetch the book using the
bookIDfrom the parameters. - Check if the book's
isArchivedproperty is alreadytrue. If it is, throw anAbort(.conflict)error with a custom reason: "Book is already archived." - Set
isArchivedtotrue, save the book, and return the updated book object.
- Fetch the book using the
- Ensure the route is properly registered within the
bootfunction of theBookController.
There are no comments for now.