If you are an iOS developer, you probably live in love with Swift syntax, type safety, and the Xcode ecosystem. However, when the time comes to create a website or a backend for your apps, you are often forced to switch contexts: learn Node.js, struggle with Python, or configure PHP.
But what if I told you that you don’t have to leave your favorite language? What if you could write your backend and your website with the same safety and power you use for your Views in SwiftUI?
Welcome to the world of Vapor, the most popular web framework for Swift programming. In this tutorial, you will learn how to build a dynamic website from scratch, leveraging your existing knowledge to become a true Full Stack developer.
What is Vapor and why should you care?
Vapor is not a static site generator. It is a full-featured, asynchronous, high-performance HTTP web framework built on SwiftNIO (Apple’s event-driven network application framework).
For an iOS developer, the advantages are overwhelming:
- One Language: Frontend (iOS) and Backend (Web) in Swift.
- Shared Code: You can share your data models (
structs) between the App and the Web. If you update a model, the compiler warns you on both sides. - Xcode: You use the best IDE in the world with debugging, autocomplete, and refactoring for your web project.
- Performance: Swift on the server is incredibly fast and memory-efficient compared to interpreted languages.
The diagram above shows how Vapor handles requests: in a non-blocking manner, allowing it to handle thousands of concurrent connections with few resources, ideal for the cloud.
Step 1: Setting Up the Development Environment
Unlike SwiftUI, where you only need Xcode, for web development, we need to install a few command-line tools.
Requirements
- macOS (Recent version).
- Xcode 14+ installed.
- Homebrew (Package manager for macOS).
Installing Vapor Toolbox
Open your terminal. We are going to install the Vapor toolbox, which will help us create and manage projects.
brew install vaporOnce installed, verify that everything is correct with:
vapor --helpStep 2: Creating Your First Web Project in Swift
Forget File > New Project in Xcode for a second. The best way to start a Vapor project is from the terminal to ensure dependencies download correctly.
Navigate to your projects folder and run:
vapor new MySwiftWebsite -nThe -n flag tells Vapor to answer “no” to configuration questions (we will use the default configuration). Once created, enter the folder:
cd MySwiftWebsite
open Package.swiftWhen you open Package.swift, Xcode will launch and begin downloading dependencies. This may take a few minutes the first time.
The Anatomy of the Project
For an iOS developer, the structure might look strange at first, but it has its equivalents:
- Package.swift: It’s like your
Podfileor project configuration. It defines dependencies. - Sources/App/configure.swift: Think of this as your
AppDelegateorApp.swift. Here you configure the database, routes, and services when the app starts. - Sources/App/routes.swift: Here you define the URLs of your website (e.g.,
/home,/contact). - Public: This is where static files go (images, CSS, JS).
Step 3: Hello World on the Web
Let’s get our server running. In Xcode, make sure to select the “Run” scheme and “My Mac” as the destination. Press Cmd + R.
You will see in the Xcode console: [ NOTICE ] Server starting on http://127.0.0.1:8080
Open your browser and go to that URL. You should see “It works!”.
Creating Your First Route
Open routes.swift. We are going to add an endpoint that returns JSON, something typical for an API that your iOS app would consume.
func routes(_ app: Application) throws {
app.get { req in
return "Hello world from Swift!"
}
app.get("ios-developer") { req -> String in
return "The best job in the world."
}
}If you save and run again, going to http://127.0.0.1:8080/ios-developer, you will see your message.
Step 4: Leaf – The View Engine (The SwiftUI of the Web)
So far we have returned plain text. But to create a website with Swift, we need HTML. This is where Leaf comes in.
Leaf is Vapor’s templating language. It is similar to HTML but allows you to inject Swift code (variables, loops, conditions). It is the equivalent of your Views in SwiftUI, but rendered on the server.
Installing Leaf
- Open
Package.swiftand add Leaf to the dependencies:
.package(url: "https://github.com/vapor/leaf.git", from: "4.0.0"),2. Add it to the “App” target:
.product(name: "Leaf", package: "leaf"),3. Open configure.swift and configure Leaf:
import Leaf // Don't forget to import
public func configure(_ app: Application) throws {
app.views.use(.leaf)
// ... rest of the code
}Creating Your First Template
Create a folder named Resources in the root of your project, and inside it a Views folder. Create a file named index.leaf.
Note: Xcode does not highlight .leaf syntax by default, but it is basically HTML.
<!DOCTYPE html>
<html>
<head>
<title>#(title)</title> <style>
body { font-family: -apple-system, sans-serif; padding: 20px; }
h1 { color: #F05138; } /* Swift Orange */
</style>
</head>
<body>
<h1>Hello, #(name)</h1>
<p>Welcome to web development with Swift.</p>
#if(isIosDeveloper):
<p>I see you're using Xcode! You are right at home.</p>
#endif
</body>
</html>Rendering the View from Swift
Go back to routes.swift. We are going to inject data into that template.
app.get("web") { req -> EventLoopFuture<View> in
let context = [
"title": "My Swift Web",
"name": "Developer",
"isIosDeveloper": "true"
]
return req.view.render("index", context)
}Run and visit /web. You just rendered dynamic HTML using Swift!
Step 5: Data Modeling (Codable to the Rescue)
This is where Swift programming shines. Instead of using [String: Any] dictionaries (which are error-prone), we will use structs that conform to Content. Content is a Vapor wrapper around Codable.
Imagine we want to show a list of iOS courses.
Create a new file Models/Course.swift:
import Vapor
struct Course: Content {
let id: Int
let name: String
let difficulty: String
}Now, let’s update routes.swift to pass an array of courses to the view:
app.get("courses") { req -> EventLoopFuture<View> in
let courses = [
Course(id: 1, name: "SwiftUI Masterclass", difficulty: "Intermediate"),
Course(id: 2, name: "Vapor from Scratch", difficulty: "Advanced")
]
// We pass the struct directly. Leaf will understand the structure.
return req.view.render("courses", ["courses": courses])
}And create Resources/Views/courses.leaf:
<h1>Course List</h1>
<ul>
#for(course in courses):
<li>
<strong>#(course.name)</strong> - Level: #(course.difficulty)
</li>
#endfor
</ul>Notice the similarity to ForEach in SwiftUI? The logic is identical: iterating over a collection of typed data.
Step 6: Full Stack Architecture (The Holy Grail)
This section is the most important for an iOS developer. The biggest nightmare in app development is keeping the API synchronized with the App. If the backend changes a field name in the JSON, the app crashes.
With Swift on the backend, we can solve this by sharing code.
Creating a Shared Package
- Create a new folder outside your Vapor project called
SharedModels. - Initialize a Swift package:
swift package init --type library. - Move your
Course.swiftstruct there (make sure it ispublicand conforms toCodable). - Import this local package into both your Vapor project (in
Package.swift) and your iOS project in Xcode.
Now, Course is the single source of truth.
- In Vapor:
struct Course: Content(Vapor adds Content conformance automatically if it is Codable). - In iOS:
let courses = try JSONDecoder().decode([Course].self, from: data).
If you change name to title in the shared package, Xcode will give you a compilation error in both the server project and the iOS app. This is type safety across the entire stack.
Step 7: Data Persistence (Fluent)
A real website needs a database. Vapor uses Fluent, an ORM (Object Relational Mapper) that allows you to interact with databases using pure Swift, without writing SQL. It’s like Core Data, but much friendlier and designed for servers.
To configure an SQLite database (ideal for testing):
- Add
FluentandFluentSQLiteDrivertoPackage.swift. - In
configure.swift:
import Fluent
import FluentSQLiteDriver
public func configure(_ app: Application) throws {
app.databases.use(.sqlite(.file("db.sqlite")), as: .sqlite)
// Register migrations
app.migrations.add(CreateCourse())
try app.autoMigrate().wait()
}Defining the Database Model
final class CourseModel: Model, Content {
static let schema = "courses"
@ID(key: .id)
var id: UUID?
@Field(key: "name")
var name: String
init() { }
init(id: UUID? = nil, name: String) {
self.id = id
self.name = name
}
}Now you can save and retrieve data using asynchronous futures:
// Save a course
app.post("api", "course") { req -> EventLoopFuture<CourseModel> in
let course = try req.content.decode(CourseModel.self)
return course.save(on: req.db).map { course }
}
// Get all courses
app.get("api", "courses") { req in
return CourseModel.query(on: req.db).all()
}Step 8: Styling Your Site (CSS and JavaScript)
Although the backend is Swift, the browser still speaks HTML/CSS. Vapor serves static files from the Public folder.
- Create
Public/styles.css. - In
configure.swift, ensure you have:app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory)). - In your
index.leaf, link the CSS:<link rel="stylesheet" href="/styles.css">.
As a developer used to SwiftUI, CSS might seem tedious. I recommend using a CSS framework like Tailwind or Bootstrap. Simply download the minified CSS, put it in the Public folder, and use it in your HTML classes in Leaf.
Optimization for Google and SEO
When you create a website with Swift using Vapor, you have a huge SEO (Search Engine Optimization) advantage compared to Single Page Applications (SPA) made in React or Angular.
Vapor renders HTML on the server (Server Side Rendering – SSR). This means that when Google’s bot visits your page, it receives the full content immediately, without having to execute JavaScript.
To optimize further:
- Dynamic Metadata: In your routes, pass variables for the title and description to Leaf.
<meta name="description" content="#(description)">2. Speed: Swift is compiled. Your server’s response time will be in milliseconds, a key factor for ranking on Google (Core Web Vitals).
Deployment: Taking Your Swift to the Cloud
Your site works on localhost. How does the world see it?
Swift runs perfectly on Linux. The standard way to deploy Vapor is using Docker.
- The Vapor project already comes with a generated
Dockerfile. - You can use services like Fly.io, Render, or Heroku.
- With Fly.io, for example, you just need to install their CLI and run
fly launch. It will detect the Dockerfile and upload your Swift site to the cloud in minutes.
Conclusion: The Future is Swift Everywhere
We have come a long way. You have configured a server, created routes, rendered HTML with Leaf, connected a database with Fluent, and understood the power of sharing code.
As an iOS developer, learning Vapor not only allows you to create a website with Swift; it transforms you into a more complete developer. You will better understand how the APIs you consume in your apps work, you will be able to prototype backends quickly, and you will have total control of your product, from the pixel on the iPhone screen to the byte in the server database.
The Swift programming ecosystem is maturing. It is no longer just for Apple devices. It is for the web, it is for the server, it is for the world.
Your next step? Try creating a web form in Vapor that saves data, and then create an App in SwiftUI that reads that same data. The feeling of seeing your Swift code flowing from end to end is unmatched.
If you have any questions about this article, please contact me and I will be happy to help you 🙂. You can contact me on my X profile or on my Instagram profile.