Swift and SwiftUI tutorials for Swift Developers

Build a website with Swift

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:

  1. One Language: Frontend (iOS) and Backend (Web) in Swift.
  2. 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.
  3. Xcode: You use the best IDE in the world with debugging, autocomplete, and refactoring for your web project.
  4. 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 vapor

Once installed, verify that everything is correct with:

vapor --help

Step 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 -n

The -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.swift

When you open Package.swiftXcode 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 Podfile or project configuration. It defines dependencies.
  • Sources/App/configure.swift: Think of this as your AppDelegate or App.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

  1. Open Package.swift and 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 ContentContent 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

  1. Create a new folder outside your Vapor project called SharedModels.
  2. Initialize a Swift package: swift package init --type library.
  3. Move your Course.swift struct there (make sure it is public and conforms to Codable).
  4. 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):

  1. Add Fluent and FluentSQLiteDriver to Package.swift.
  2. 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.

  1. Create Public/styles.css.
  2. In configure.swift, ensure you have: app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory)).
  3. 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:

  1. 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.

  1. The Vapor project already comes with a generated Dockerfile.
  2. You can use services like Fly.ioRender, or Heroku.
  3. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Article

.animation() vs withAnimation() in SwiftUI

Next Article

.Animation vs .Transition in SwiftUI

Related Posts