How To Upload Images, Store Them and Serve Them With Vapor 4 | by Fernando Moya de Rivas | Apr, 2022

For the past few weeks, I’ve been researching and sharing articles about Vapor 4 and its capabilities. I started with a simple project structure and API definition:

and built it up from that to explain DB Model relationships:

In this article, we’ll expand on the original project and will learn how to upload images, store them and serve them. Finally, I’ll mention some other interesting features I didn’t get to cover in my research. If you get lost, you can always check the final project here:

https://github.com/fermoya/vapor-tutorial

Our goal here is to create a new endpoint /todo-lists/<id>/upload-image that will accept a multipart form that with the image. Let’s first create the endpoint. Go to TodoListViewController.boot(routes:) method and create a new route:

func boot(routes: RoutesBuilder) throws {
...
let singleListRoutes = ...
singleListRoutes.get("upload-image", use: uploadImage)
}
private func uploadImage(req: Request) throws -> EventLoopFuture<Response> {
...
}

Now, all we need to do is extract the image data, but how do we do this? And once we have the file, how do we store it and serve it?

To do so, we can make use of FileMiddleware . A Middleware is just a class that lets you process an incoming request before it reached the route handler. You can think of it as a Proxy. You can create your own, say, to have all responses add a certain header common to all responses:

For our purposes, Vapor ships with FileMiddleware which lets you serve assets to clients from a public folder. We will be using the workingDirectory for simplicity, but ideally, you’d serve your files from a specific Public directory.

Similar to databases or migrations, any kind of configuration needs to be done in the configure(_:) function inside configure.swift :

app.middleware.use(FileMiddleware(publicDirectory: app.directory.workingDirectory))

Besides, we need to increase the defaultMaxBodySize for requests:

app.routes.defaultMaxBodySize = "10mb"

Otherwise, you’ll get a Payload Too Large error. Now all we need to do is decode the image from the request and store it on the server. Let’s do this:

// 1.
let file = try req.content.decode(File.self)
let path = req.application.directory.workingDirectory + file.filename
// 2.
return req.fileio
.writeFile(file.data, at: file.filename)
// 3.
.transform(to: Response(status: .accepted))

Let’s break it down:

  • First, we decode a File from the response. Vapor ships with this utility. Besides, we form the path where the file will be stored.
  • Next, we make use fileIO to write save the file into a path in our server
  • Finally, we return an accepted response

Enough talking, shall we try it out? I downloaded a sample image which I can then use for my TodoList :

$ curl localhost:8080/v1/todo-lists 
-X POST
-H "Content-Type:application/json"
--data "{ "name": "Foo" }"
{"id":"F65B591A-AFE9-4848-AFCB-4FC606002596","name":"Foo"}
$ curl localhost:8080/v1/todo-lists/F65B591A-AFE9-4848-AFCB-4FC606002596/upload-image
-F filename=example.webp
-F data=@/Users/fermoya/Desktop/example.webp
-H "Content-Type:multipart/form-data"

Nice! But where’s my image now? Well, the file path we used was /<working_directory>/<file_name> . That means that the file will be available under localhost:8080/<file_name> :

However, the image is not linked to any TodoList just yet! We’re in fact just storing the image but it will get lost persisted into our TodoList model.

At the moment, TodoList isn’t ready to hold an image just yet. We need to add a new field to the model. This is easy enough if you remember from previous articles:

Notice that we’re simply updating an existing table. This isn’t really necessary for our example as we’re using an in-memory database, but you’d need it for a real App. Make sure you update the table only after you create it in CreateTodoListMigration .

Now, we’d only need to update the /upload-image implementation:

What’s new here?

  • First, we’re fetching the list and aborting if notFound
  • Secondly, update the list after saving the image. This is somewhat related to an exercise we left for the reader in Part 1
  • Return the updated list as a response

We’re done! If you try this time, you should get something like:

$ curl localhost:8080/v1/todo-lists 
-X POST
-H "Content-Type:application/json"
--data "{ "name": "Foo" }"
{"id":"F65B591A-AFE9-4848-AFCB-4FC606002596","name":"Foo", "imageURL": null}
$ curl localhost:8080/v1/todo-lists/F65B591A-AFE9-4848-AFCB-4FC606002596/upload-image
-F filename=example.webp
-F data=@/Users/fermoya/Desktop/example.webp
-H "Content-Type:multipart/form-data" | jq
{
"id": "F65B591A-AFE9-4848-AFCB-4FC606002596",
"name": "Foo",
"imageURL": "127.0.0.1:8080/example.webp"
}

This is neat, although there’s a big flaw in this implementation: we’re not really properly handling the images, and it’d be fairly simple to overwrite the changes if, say, an image with the same name is uploaded for a totally different list.

One way to solve this would be to ignore the filename field in the multipart/form-data and compose the name like MD5_Hash(#"<uuid>.<timestamp>") but we’ll leave this as an exercise for the reader.

There are so many features we haven’t covered in this series due to the lack of time, but these are worth mentioning:

  • Leaf: We focused on the backend side of Vapor but the framework ships with Leaf which helps you turn Swift into dynamic HTML. This will help you create the frontend side of your web app.
  • Logging: Both Request and Application come with a logger property with several trace levels: INFO , ERROR , DEBUG … for instance, req.logger.debug("Updating list with id (id)")
  • Advanced routing: Some things that we didn’t cover are:
    – Redirections: for instance, an endpoint has been renamed and you wish to simply reroute to the new one.
    – Catchalls: use ** as dynamic routes to match one or more components, ie /foo/** responds to /foo/bar , /foo/bar/abc … You can then use getCatchall to retrieve them as an array of String
    – View all routes: you can print all registered routes by print(app.routes.all)
  • HTTP Client: who said you won’t need to make an HTTP request from one of your endpoints? Vapor ships with an easy-to-use HTTP client.
  • Websockets: two-way communications between a client and your server, say, for a chat app for instance.
  • Queues: very useful if your endpoint triggers some heavy operations. You can respond quickly to the client and schedule a Job . A very common example would be a reset-password endpoint where an email needs to be sent.
  • Services: they come in handy to reuse code in several endpoints or use third-party libraries.
  • APNS: Developed in Swift, most likely for someone who’s worked with iOS/macOS, Vapor makes it easy to give support to Apple notifications.

Leave a Comment