Chapters

Hide chapters

Server-Side Swift with Vapor

Third Edition - Early Acess 1 · iOS 13 · Swift 5.2 - Vapor 4 Framework · Xcode 11.4

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section I: Creating a Simple Web API

Section 1: 13 chapters
Show chapters Hide chapters

25. Adding Profile Pictures
Written by Tim Condon

Note: This update is an early-access release. This chapter has not yet been updated to Vapor 4.

In previous chapters, you learned how to send data to your Vapor application in POST requests. You used JSON bodies and forms to transmit the data, but the data was always simple text. In this chapter, you’ll learn how to send files in requests and handle that in your Vapor application. You’ll use this knowledge to allow users to upload profile pictures in the web application.

Note: This chapter teaches you how to upload files to the server where your Vapor application runs. For a real application, you should consider forwarding the file to a storage service, such as AWS S3. Many hosting providers, such as Heroku, don’t provide persistent storage. This means that you’ll lose your uploaded files when redeploying the application. You’ll also lose files if the hosting provider restarts your application. Additionally, uploading the files to the same server means you can’t scale your application to more than one instance because the files won’t exist across all application instances.

Adding a picture to the model

As in previous chapters, you need to change the model so you can associate an image with a User. Open the Vapor TIL application in Xcode and open User.swift. Add the following below var email: String:

var profilePicture: String?

This stores an optional String for the image. It will contain the filename of the user’s profile picture on disk. The filename is optional as you’re not enforcing that a user has a profile picture — and they won’t have one when they register. Replace the initializer to account for the new property:

init(name: String,
     username: String,
     password: String,
     email: String,
     profilePicture: String? = nil) {
  self.name = name
  self.username = username
  self.password = password
  self.email = email
  self.profilePicture = profilePicture
}

Providing a default value of nil for profilePicture allows your app to continue to compile and operate without further source changes.

Note: You could use the user APIs from Google and GitHub to get a URL to the user’s profile picture. This would allow you to download the image and store it along side regular users’ pictures or save the link. However, this is left as an exercise for the reader.

You could make uploading a profile picture part of the registration experience, but this chapter does it in a separate step. Note how createHandler(_:user:) in UsersController doesn’t need to change for the new property. This is because the route handler uses Codable and sets the property to nil if the data isn’t present in the POST request.

Reset the database

As in the past, since you’ve added a property to User, you must reset the database. Instead of deleting the Docker container as you did in Chapter 24, “Password Reset & Emails”, this chapter uses the revert command. Option-Click the Run button in Xcode to open the scheme editor. On the Arguments tab, click + in the Arguments Passed On Launch section. Enter:

revert --all --yes

Click Run and you’ll see the output in the Xcode console showing the reversions. Option-Click the Run button once more and clear the checkbox next to the arguments you entered. Next time the application starts, it will prepare the database with the new column.

Note: There’s no difference in outcome to reverting the database or resetting the Docker container. Whichever one you choose is down to personal preference.

Verify the tests

Since you changed your User model, you should run your tests to ensure the change didn’t break anything. In Xcode, select the TILApp-Package scheme. Next, make sure the Docker container for the test database is running. In Terminal, type:

docker ps -a

You should see both your main database container, postgres, and the test database container, postgres-test. Ensure that postgres-test has a status similar to Up 2 hours. If the status is Exited, you can start the container again with the following:

docker start postgres-test

Finally, in Xcode, Option-Click the TILApp-Package scheme and select the Test action. Ensure Use the Run action’s arguments and environment variables is unchecked. Then, make sure the required environment variables are set. They can be random values for the tests:

  • GOOGLE_CALLBACK_URL
  • GOOGLE_CLIENT_ID
  • GOOGLE_CLIENT_SECRET
  • GITHUB_CALLBACK_URL
  • GITHUB_CLIENT_ID
  • GITHUB_CLIENT_SECRET
  • SENDGRID_API_KEY

Click Close, then type Command+U to run all the tests. They should all pass.

Creating the form

With the model changed, you can now create a page to allow users to submit a picture. In Xcode, open WebsiteController.swift. Below resetPasswordPostHandler(_:data:) add the following:

func addProfilePictureHandler(_ req: Request) throws
  -> Future<View> {
    return try req.parameters.next(User.self)
      .flatMap { user in
        try req.view().render(
          "addProfilePicture", 
          ["title": "Add Profile Picture",
           "username": user.name])
   }
}

This defines a new route handler that renders addProfilePicture.leaf. The route handler also passes the title and the user’s name to the template as a dictionary. Next, add the following to the end of boot(router:), to register the new route handler:

protectedRoutes.get(
  "users",
  User.parameter,
  "addProfilePicture",
  use: addProfilePictureHandler)

This connects a GET request to /users/<USER_ID>/addProfilePicture to addProfilePictureHandler(_:). Note that the route is also a protected route — users must be logged in to add profiles pictures to users.

The TIL application also allows users to upload profile pictures for any user, not just their own.

In Resources/Views, create the new template, addProfilePicture.leaf. Open the new file in an editor and insert the following:

#// 1
#set("content") {
  #// 2
  <h1>#(title)</h1>

  #// 3
  <form method="post" enctype="multipart/form-data">
    #// 4
    <div class="form-group">
      <label for="picture">
        Select Picture for #(username)
      </label>
      <input type="file" name="picture"
       class="form-control-file" id="picture"/>
    </div>

    #// 5
    <button type="submit" class="btn btn-primary">
      Upload
    </button>
  </form>
}

#// 6
#embed("base")

Here’s what the new template does:

  1. Set content as required by base.leaf.
  2. Use the title passed to the template as the title for the page.
  3. Create a form and set the method to POST. When you submit the form, the browser sends the form as a POST request to the same URL. Note the encoding type of multipart/form-data. This allows you to send files to the server from the browser.
  4. Create a form group with an input type of file. This presents a file browser in your web browser. Bootstrap uses form-control-file to help style the input.
  5. Add a submit button to allow users to submit the form.
  6. Embed base.leaf to include the main template.

Next, you need a link for users to be able to access the new form. Open WebsiteController.swift, add a new property at the bottom of UserContext:

let authenticatedUser: User?

This stores the authenticated user for that request, if one exists. In userHandler(_:), replace let context = ... with the following:

// 1
let loggedInUser = try req.authenticated(User.self)
// 2
let context = UserContext(
  title: user.name,
  user: user,
  acronyms: acronyms,
  authenticatedUser: loggedInUser)

Here’s what you changed:

  1. Get the authenticated user from Request. This returns User? as there may be no authenticated user.
  2. Pass the optional, authenticated user to the context.

Finally, open user.leaf. Add the following before #embed("acronymsTable"):

#if(authenticatedUser) {
  <a href="/users/#(user.id)/addProfilePicture">
    #if(user.profilePicture){Update } else{Add } Profile Picture
  </a>
}

This adds a link to the new add profile picture page if the user is logged in. The link will display Update Profile Picture if a user already has a profile picture, otherwise the link displays Add Profile Picture.

In Xcode, select the Run scheme and build and run the application. In the browser, head to http://localhost:8080/login and log in as the admin user. Once logged in, click All Users and select the admin user.

There’s a new link to the add profile picture page. Click Add Profile Picture and you’ll see the new form to add a profile picture:

Accepting file uploads

Next, implement the necessary code to handle the POST request from the form. In Terminal, enter the following:

# 1
mkdir ProfilePictures
# 2
touch ProfilePictures/.keep

Here’s what these commands do:

  1. Create the directory to store the users’ profile pictures.
  2. Add an empty file so the directory is added to source control. This helps with deploying applications to ensure the directory exists.

Next, in Xcode, open WebsiteController.swift. At the bottom of the file add the following:

struct ImageUploadData: Content {
  var picture: Data
}

This new type represents the data sent by the form. picture matches the name of the input specified in the HTML form.

Since the form uploads a file, you’ll decode the picture into Data.

Next, add a new property at the top of WebsiteController, above boot(router:):

let imageFolder = "ProfilePictures/"

This defines the folder where you’ll store the images. Next, below addProfilePictureHandler(_:) add a request handler for the POST request:

func addProfilePicturePostHandler(_ req: Request)
  throws -> Future<Response> {
    // 1
    return try flatMap(
      to: Response.self,
      req.parameters.next(User.self),
      req.content.decode(ImageUploadData.self)) {
        user, imageData in
        // 2
        let workPath =
          try req.make(DirectoryConfig.self).workDir
        // 3
        let name =
          try "\(user.requireID())-\(UUID().uuidString).jpg"
        // 4
        let path = workPath + self.imageFolder + name
        // 5
        FileManager().createFile(
          atPath: path,
          contents: imageData.picture,
          attributes: nil)
        // 6
        user.profilePicture = name
        // 7
        let redirect =
          try req.redirect(to: "/users/\(user.requireID())")
        return user.save(on: req).transform(to: redirect)
    }
}

Here’s what the new request handler does:

  1. Get the user from the parameters and decode the request body to ImageUploadData.

  2. Get the current working directory of the application.

  3. Create a unique name for the profile picture.

  4. Set up the path of the file to save.

  5. Save the file on disk using the path and the image data.

  6. Update the user with the profile picture filename.

  7. Save the updated user and return a redirect to the user’s page.

Finally, register the route at the bottom of boot(router:):

protectedRoutes.post(
  "users",
  User.parameter,
  "addProfilePicture",
  use: addProfilePicturePostHandler)

This connects a POST request to /users/<USER_ID>/addProfilePicture to addProfilePicturePostHandler(_:).

Displaying the picture

Now that a user can upload a profile picture, you need to be able to serve the image back to the browser. Normally, you would use the FileMiddleware. However, as you’re storing the images in a different directory, this chapter teaches you how to serve them manually.

In WebsiteController.swift add a new route handler below addProfilePicturePostHandler(_:):

func getUsersProfilePictureHandler(_ req: Request)
  throws -> Future<Response> {
    // 1
    return try req.parameters.next(User.self)
      .flatMap(to: Response.self) { user in
        // 2
        guard let filename = user.profilePicture else {
          throw Abort(.notFound)
        }
        // 3
        let path = try req.make(DirectoryConfig.self)
          .workDir + self.imageFolder + filename
      // 4
      return try req.streamFile(at: path)
    }
}

Here’s what the new route handler does:

  1. Get the user from the request’s parameters.
  2. Ensure the user has a saved profile picture, otherwise throw a 404 Not Found error.
  3. Construct the path of the user’s profile picture.
  4. Use Vapor’s FileIO function to return the file as a Response. This handles reading the file and returning the correct information to the browser.

Next, register the new route in boot(router:) below authSessionRoutes.post(ResetPasswordData.self, at: "resetPassword", use: resetPasswordPostHandler):

authSessionRoutes.get(
  "users",
  User.parameter,
  "profilePicture",
  use: getUsersProfilePictureHandler)

This connects a GET request to /users/<USER_ID>/profilePicture to getUsersProfilePictureHandler(_:). Finally, open user.leaf. Before <h1>#(user.name)</h1> add the following:

#if(user.profilePicture) {
  <img src="/users/#(user.id)/profilePicture"
   alt="#(user.name)">
}

This checks if the user passed to the template’s context has a profile picture. If so, Leaf adds a link to the image to the page.

Build and run the application and go to http://localhost:8080/login in your browser. Log in as the default admin user then navigate to the admin user’s profile page. Click Add Profile Picture and in the form click Choose File. Select an image to upload then click Upload.

The website will redirect you to the user’s profile page, where you’ll see the uploaded image:

Where to go from here?

In this chapter, you learned how to deal with files in Vapor. You saw how to handle file uploads and save files to disk. You also learned how to serve files from disk in a route handler.

You’ve now built a fully-featured API that demonstrates many of the capabilities of Vapor. You’ve built an iOS application to consume the API, as well as a front-end website using Leaf. You’ve also learned how to test your application.

These sections have given you all the knowledge you need to build the back ends and web sites for your own applications! The next chapters cover more advanced topics that you may need, such as database migrations and caching. You’ll also learn how to deploy your application to the internet.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.