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

36. Microservices, Part 2
Written by Tim Condon

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

In the previous chapter, you learned the basics of microservices and how to apply the architecture to the TIL application. In this chapter, you’ll learn about API gateways and how to make microservices accessible to clients. Finally, you’ll learn how to use Docker and Docker Compose to spin up the whole application.

The API gateway

The previous chapter introduced two microservices for the TIL application, one for acronyms and one for users. In a real application, you may have many more services for all different aspects of your application. It’s difficult for clients to integrate with an application made up of such a large number of microservices. Each client needs to know what each microservice does and the URL of each service. The client may even have to use different authentication methods for each service. A microservices architecture makes it hard to split a service into separate services. For example, moving authentication out of the users service in the TIL application would require an update to all clients.

One solution to this problem is the API gateway. An API gateway can aggregate requests from clients and distribute them to all required services. Additionally, an API gateway can retrieve results from multiple services and combine them into a single response.

Most cloud providers offer API gateway solutions to manage large numbers of microservices, but you can easily create your own. In this chapter, you’ll do just that.

Download the starter project for this chapter. The TILAppUsers and TILAppAcronyms projects are the same as the final projects from the previous chapter. There’s a new TILAppAPI project that contains the skeleton for the API gateway.

Starting the services

In Terminal, open three separate tabs. Ensure the MySQL, Postgres and Redis Docker containers are running from the previous chapter. In Terminal, type the following:

docker ps

This command displays the currently running containers. You should see the three containers running:

Next, in the first tab, navigate to the TILAppUsers directory and run the following command:

swift run

This starts the TILAppUsers service. In the second tab, navigate to the TILAppAcronyms and run the following command:

swift run

This starts the TILAppAcronyms service. Finally, in the third tab, navigate to the TILAppAPI directory and enter this command:

vapor xcode -y

This downloads the dependencies for the app, then generates and opens the Xcode project.

Forwarding requests

In the TILAppAPI Xcode project, open UsersController.swift. Below boot(router:) enter the following:

// 1
func getAllHandler(_ req: Request) throws -> Future<Response> {
  return try req.client().get("\(userServiceURL)/users")
}

// 2
func getHandler(_ req: Request) throws -> Future<Response> {
  let id = try req.parameters.next(UUID.self)
  return try req.client().get("\(userServiceURL)/users/\(id)")
}

// 3
func createHandler(_ req: Request) throws -> Future<Response> {
  return try req.client().post("\(userServiceURL)/users") {
    createRequest in
    // 4
    try createRequest.content.encode(
      req.content.syncDecode(CreateUserData.self))
  }
}

Here’s what happening in the new code:

  1. Create a route handler to get all the users. Simply return the response from the /users route of the TILAppUsers microservice.
  2. Create a route handler to get a single user. Get the UUID of the user from the request’s parameters and return the response from the TILAppUsers microservice for that user.
  3. Create a route handler to create a user. Send a POST request to the users route of the TILAppUsers service and return the response.
  4. Before you send the request, encode the data from the request to the API gateway into the request to the TILAppUsers service. This is the data required to create a user.

Register the new routes inside boot(router:), below let routeGroup = router.grouped("api", "users"):

// 1
routeGroup.get(use: getAllHandler)
// 2
routeGroup.get(UUID.parameter, use: getHandler)
// 3
routeGroup.post(use: createHandler)

Here’s what this does:

  1. Route a GET request to /api/users/ to getAllHandler(_:).
  2. Route a GET request to /api/users/<USER_ID> to getHandler(_:).
  3. Route a POST request to /api/users/ to createHandler(_:).

These requests don’t need any authentication or multiple services. You can forward them directly onto the TILAppUsers microservice.

Open AcronymsController.swift to do the same for the GET requests. Below boot(router:) add the following:

// 1
func getAllHandler(_ req: Request) throws -> Future<Response> {
  return try req.client().get("\(acronymsServiceURL)/")
}

// 2
func getHandler(_ req: Request) throws -> Future<Response> {
  let id = try req.parameters.next(Int.self)
  return try req.client().get("\(acronymsServiceURL)/\(id)")
}

Here’s what the new code does:

  1. Create a route handler to get all the acronyms. Simply return the response from the / route of the TILAppAcronyms microservice.
  2. Create a route handler to get a single acronym. Get the id of the acronym from the request’s parameters. Return the response from the TILAppAcronyms microservice for that acronym.

Register the new routes in boot(router:), below let acronymsGroup = router.grouped("api", "acronyms"):

// 1
acronymsGroup.get(use: getAllHandler)
// 2
acronymsGroup.get(Int.parameter, use: getHandler)

Here’s what this does:

  1. Route a GET request to /api/acronyms/ to getAllHandler(_:).
  2. Route a GET request to /api/acronyms/<ACRONYM_ID> to getHandler(_:).

Build and run the application and launch RESTed. Configure a new request as follows:

Click Send Request and you’ll see all the users in the TILAppUsers microservice:

API Authentication

Logging in

Authentication for the API gateway works in exactly the same way as the microservices. First, you must allow a user to log in.

In Xcode, open UsersController.swift. Below createHandler(_:) add a new route handler to handle logging in:

func loginHandler(_ req: Request) throws -> Future<Response> {
  // 1
  return try req.client().post("\(userServiceURL)/auth/login") {
    loginRequest in
      // 2
      guard let authHeader =
        req.http.headers[.authorization].first else {
          throw Abort(.unauthorized)
      }
      // 3
      loginRequest.http.headers.add(name: .authorization,
        value: authHeader)
  }
}

Here’s what the new route handler does:

  1. Send a POST request to the TILAppUsers microservice to log the user in.
  2. Ensure the incoming request contains an Authorization header. Otherwise return a 401 Unauthorized response.
  3. Encode the outgoing request with the authorization header from the incoming request. This header contains the HTTP Basic Authentication information for the user.

Register the route in boot(router:) below routeGroup.post(use: createHandler) as follows:

routeGroup.post("login", use: loginHandler)

This routes a POST request to /api/users/login to loginHandler(_:). Build and run the application and launch RESTed. Configure a new request as follows:

Click Authorization and enter the username and password for the user created in the previous chapter. Check Present Before Authentication Challenge and click OK.

Click Send Request and you’ll receive a token for that user. Copy the token value:

Accessing protected routes

Back in Xcode, open AcronymsController.swift. Below getHandler(_:), create a new route handler to create an acronym:

func createHandler(_ req: Request) throws -> Future<Response> {
  // 1
  return try req.client().post("\(acronymsServiceURL)/") {
    createRequest in
      // 2
      guard let authHeader =
        req.http.headers[.authorization].first else {
          throw Abort(.unauthorized)
      }
      // 3
      createRequest.http.headers.add(
        name: .authorization,
        value: authHeader)
      // 4
      try createRequest.content.encode(
        req.content.syncDecode(CreateAcronymData.self))
  }
}

Here’s what the code does:

  1. Send a POST request to the TILAppAcronyms microservice to create a new acronym.
  2. Ensure the incoming request contains an Authorization header, otherwise return a 401 Unauthorized response.
  3. Add the Authorization header to the outgoing request to the TILAppAcronyms microservice.
  4. Encode the body of the outgoing request with the data to create an acronym. The data comes from the incoming request.

Register the route handler in boot(router:) below acronymsGroup.get(Int.parameter, use: getHandler):

acronymsGroup.post(use: createHandler)

This routes a POST request to /api/acronyms to createHandler(_:). Build and run the app and return to RESTed. Click Authorization and uncheck Present Before Authentication Challenge to stop RESTed sending the HTTP Basic Authentication credentials in the header.

Then, configure a new request as follows:

Add two parameters with names and values:

  • short: IRL
  • long: In Real Life

Create a new header field for Authorization with the value Bearer <TOKEN STRING>,\ using the token string you copied earlier.

Click Send Request and you’ll see the acronym created in the TILAppAcronyms microservice via the API gateway:

Back in Xcode, create the route handlers for updating and deleting acronyms. Below createHandler(_:), add the following:

func updateHandler(_ req: Request) throws -> Future<Response> {
  // 1
  let acronymID = try req.parameters.next(Int.self)
  // 2
  return try req.client()
    .put("\(acronymsServiceURL)/\(acronymID)") {
      updateRequest in
        // 3
        guard let authHeader =
          req.http.headers[.authorization].first else {
            throw Abort(.unauthorized)
        }
        // 4
        updateRequest.http.headers.add(
          name: .authorization,
          value: authHeader)
        // 5
        try updateRequest.content.encode(
          req.content.syncDecode(CreateAcronymData.self))
  }
}

func deleteHandler(_ req: Request) throws -> Future<Response> {
  // 6
  let acronymID = try req.parameters.next(Int.self)
  // 7
  return try req.client()
    .delete("\(acronymsServiceURL)/\(acronymID)") {
      deleteRequest in
        // 8
        guard let authHeader =
          req.http.headers[.authorization].first else {
            throw Abort(.unauthorized)
        }
        // 9
        deleteRequest.http.headers.add(
          name: .authorization,
          value: authHeader)
  }
}

Here’s what the new code does:

  1. Get the ID of the acronym from the request’s parameters.
  2. Send a request to the TILAppAcronyms microservice to update that acronym. Return the response.
  3. Ensure the incoming request contains an Authorization header before you send the request. If not, return a 401 Unauthorized response.
  4. Add the Authorization header to the outgoing request.
  5. Encode the body of the outgoing request with the data to update the acronym. The data comes from the incoming request.
  6. Get the ID of the acronym from the request’s parameters.
  7. Send a request to the TILAppAcronyms microservice delete that acronym. Return the response.
  8. Ensure the incoming request contains an Authorization header before you send the request. If not, return a 401 Unauthorized response.
  9. Add the Authorization header to the outgoing request.

Finally, register the new routes in boot(router:) below acronymsGroup.post(use: createHandler):

// 1
acronymsGroup.put(Int.parameter, use: updateHandler)
// 2
acronymsGroup.delete(Int.parameter, use: deleteHandler)

Here’s what this does:

  1. Route a PUT request to /api/acronyms/<ID> to updateHandler(_:).
  2. Route a DELETE request to /api/acronyms/<ID> to deleteHandler(_:).

Handling relationships

In the previous chapter, you saw how relationships work with microservices. Getting relationships for different models is difficult for clients in an microservices architecture. You can use the API gateway to help simplify this.

Getting a user’s acronyms

In Xcode, open UsersController.swift. Below loginHandler(_:) add a new route handler to get a user’s acronyms:

func getAcronyms(_ req: Request) throws -> Future<Response> {
  // 1
  let userID = try req.parameters.next(UUID.self)
  // 2
  return try req.client()
    .get("\(acronymsServiceURL)/user/\(userID)")
}

Here’s what’s going on:

  1. Get the ID of the user from the request’s parameters.
  2. Send a request to the TILAppAcronyms microservice to get all the acronyms for that user and return the response.

Register the route in boot(router:) below routeGroup.post("login", use: loginHandler):

routeGroup.get(UUID.parameter, "acronyms", use: getAcronyms)

This routes a GET request to /api/users/<USER_ID>/acronyms to getAcronyms(_:).

Getting an acronym’s user

Getting a user’s acronyms looks the same as other requests in the microservice as the client knows the user’s ID. Getting the user for a particular acronym is more complicated. Open AcronymsController.swift and add a new route handler to do this below deleteHandler(_:):

func getUserHandler(_ req: Request) throws -> Future<Response> {
  // 1
  let acronymID = try req.parameters.next(Int.self)
  // 2
  return try req
    .client()
    .get("\(acronymsServiceURL)/\(acronymID)")
    .flatMap(to: Response.self) { response in
      // 3
      let acronym =
        try response.content.syncDecode(Acronym.self)
      // 4
      return try req
        .client()
        .get("\(self.userServiceURL)/users/\(acronym.userID)")
  }
}

Here’s what the new route handler does:

  1. Get the ID of the acronym from the request’s parameters.
  2. Make a request to TILAppAcronyms to get the details for that acronym.
  3. Decode the response to an Acronym.
  4. Make a request to TILAppUsers using the user ID from the decoded acronym.

This route handler requires a request to both microservices. The API gateway makes this a simple request to make for clients, much like the monolithic TIL application. Register the route in boot(router:) below acronymsGroup.delete(Int.parameter, use: deleteHandler):

acronymsGroup.get(Int.parameter, "user", use: getUserHandler)

This routes a GET request to /api/acronyms/<ACRONYM_ID>/user to getUserHandler(_:). Build and run the app and launch RESTed. Configure a new request as follows:

Click Send Request. The API gateway makes the necessary requests to all the microservices to get the user for the acronym with that ID. You’ll see the user information returned:

Finally, stop the TILAppAPI application in Xcode.

Running everything in Docker

You now have three microservices that make up your TIL application. These microservices also require another three databases to work. If you’re developing a client application, or another microservice, there’s a lot to run to get started. You may also want to run everything in Linux to check your services deploy correctly. Like in Chapter 11, “Testing”, you’re going to use Docker Compose to run everything.

Injecting in service URLs

Currently the application hardcodes the URLs for the different microservices to localhost. You must change this to run them in Docker Compose. Back in Xcode in TILAppAPI, open AcronymsController.swift. Replace the definitions of userServiceURL and acronymsServiceURL with the following:

let acronymsServiceURL: String
let userServiceURL: String

init(
  acronymsServiceHostname: String,
  userServiceHostname: String) {
    acronymsServiceURL =
      "http://\(acronymsServiceHostname):8082"
    userServiceURL = "http://\(userServiceHostname):8081"
}

This allows you to inject in the host names for the different services. Open UsersController.swift and again replace the definitions of userServiceURL and acronymsServiceURL with the following:

let userServiceURL: String
let acronymsServiceURL: String

init(
  userServiceHostname: String,
  acronymsServiceHostname: String) {
    userServiceURL = "http://\(userServiceHostname):8081"
    acronymsServiceURL =
      "http://\(acronymsServiceHostname):8082"
}

Finally, open routes.swift and replace the body of routes(_:) with the following:

let usersHostname: String
let acronymsHostname: String

// 1
if let users = Environment.get("USERS_HOSTNAME") {
  usersHostname = users
} else {
  usersHostname = "localhost"
}

// 2
if let acronyms = Environment.get("ACRONYMS_HOSTNAME") {
  acronymsHostname = acronyms
} else {
  acronymsHostname = "localhost"
}

// 3
try router.register(collection: UsersController(
  userServiceHostname: usersHostname,
  acronymsServiceHostname: acronymsHostname))
try router.register(collection: AcronymsController(
  acronymsServiceHostname: acronymsHostname,
  userServiceHostname: usersHostname))

Here’s what changed:

  1. Use USERS_HOSTNAME for the users microservice host name, if the environment variable exists. Otherwise, default to localhost.
  2. Use ACRONYMS_HOSTNAME for the acronyms microservice host name, if the environment variable exists. Otherwise, default to localhost.
  3. Register UsersController and AcronymsController as RouteCollections, injecting in the hostnames.

Build the project to ensure everything compiles and close Xcode. Next, open TILAppAcronyms in Xcode and open UserAuthMiddleware.swift. Before respond(to:) add the following:

let authHostname: String

init(authHostname: String) {
  self.authHostname = authHostname
}

This allows you to pass in the hostname for the TILAppUsers microservice. Next replace the URL that the middleware makes a request to, "http://localhost:8081/auth/authenticate", with the following:

"http://\(authHostname):8081/auth/authenticate"

This uses the hostname passed in to make the request to. Finally, open AcronymsController.swift and inside boot(router:), replace let authGroup = router.grouped(UserAuthMiddleware()) with the following:

let authHostname: String
// 1
if let host = Environment.get("AUTH_HOSTNAME") {
  authHostname = host
} else {
  authHostname = "localhost"
}
// 2
let authGroup = router.grouped(
  UserAuthMiddleware(authHostname: authHostname))

Here’s what the new code does:

  1. Check for an AUTH_HOSTNAME environment variable and use the value for authHostname. Default to localhost if the environment variable doesn’t exist.
  2. Create a route group using UserAuthMiddleware and pass in authHostname.

Build the project to ensure the code compiles.

The Docker Compose file

In the root directory containing all three projects, create a new file called docker-compose.yml and open it in an editor. First, define the version and database services:

# 1
version: '3'
services:
  # 2
  postgres:
    image: "postgres"
    environment:
      - POSTGRES_DB=vapor
      - POSTGRES_USER=vapor
      - POSTGRES_PASSWORD=password
  # 3
  mysql:
    image: "mysql/mysql-server:5.7"
    environment:
      - MYSQL_USER=vapor
      - MYSQL_PASSWORD=password
      - MYSQL_DATABASE=vapor
  # 4
  redis:
    image: "redis"

Here’s what’s happening:

  1. Set the version number for the Docker Compose file.
  2. Define a service for the PostgreSQL database. Use the postgres image and the same environment variables as your local Docker container.
  3. Define a service for the MySQL database. Use the mysql/mysql-server:5.7 image and the same environment variables as your local Docker container.
  4. Define a service for the Redis database. Use the redis image.

At the end of the file, add the following for the TILAppUsers microservice:

  # 1
  til-users:
    # 2
    depends_on:
      - postgres
      - redis
    # 3
    build:
      context: ./TILAppUsers
      dockerfile: web.Dockerfile
    # 4
    environment:
      - DATABASE_HOSTNAME=postgres
      - REDIS_HOSTNAME=redis
      - PORT=8081
      - ENVIRONMENT=production
      - DATABASE_PASSWORD=password

Note: The indentation must match the other services defined.

Here’s what the new code does:

  1. Define a service for TILAppUsers.
  2. Tell Docker Compose this service depends on the postgres and redis containers. Docker Compose will start those services before TILAppUsers.
  3. Tell Docker Compose the working directory for the service and the Dockerfile to use. The default Vapor template contains a compatible Dockerfile.
  4. Set the necessary environment variables for the service. These define the variables required for the databases and the environment and port.

You may notice that this service does not expose any ports outside of Docker Compose. Since you’re routing everything via the API gateway, there’s no need to expose the other microservices.

At the end of the file, add the specification for TILAppAcronyms:

  # 1
  til-acronyms:
    # 2
    depends_on:
      - mysql
      - til-users
    # 3
    build:
      context: ./TILAppAcronyms
      dockerfile: web.Dockerfile
    # 4
    environment:
      - DATABASE_HOSTNAME=mysql
      - PORT=8082
      - ENVIRONMENT=production
      - AUTH_HOSTNAME=til-users

Here’s what the new specification does:

  1. Define a service for TILAppAcronyms.
  2. Tell Docker Compose this service depends on the mysql and til-users containers. Docker Compose will start those services before TILAppAcronyms.
  3. Tell Docker Compose the working directory for the service and the Dockerfile to use. The default Vapor template contains a compatible Dockerfile.
  4. Set the necessary environment variables for the service. These define the variables required for the database and the environment and port. This also sets the AUTH_HOSTNAME environment variable so this service can send requests to TILAppUsers.

Finally, at the bottom of the file add the specification for TILAppAPI:

  # 1
  til-api:
    # 2
    depends_on:
      - til-users
      - til-acronyms
    # 3
    ports:
      - "8080:8080"
    # 4
    build:
      context: ./TILAppAPI
      dockerfile: web.Dockerfile
    # 5
    environment:
      - USERS_HOSTNAME=til-users
      - ACRONYMS_HOSTNAME=til-acronyms
      - PORT=8080
      - ENVIRONMENT=production

Here’s what the new specification does:

  1. Define a service for TILAppAPI.
  2. Tell Docker Compose this service depends on the til-users and til-acronyms containers. Docker Compose will start those services before TILAppAcronyms.
  3. Expose the container’s 8080 port to your local machine on port 8080. This allows you to connect to the container.
  4. Tell Docker Compose the working directory for the service and the Dockerfile to use. The default Vapor template contains a compatible Dockerfile.
  5. Set the necessary environment variables for the service. This defines the environment and port. This also sets the USERS_HOSTNAME and ACRONYMS_HOSTNAME environment variables so this service can send requests to TILAppUsers and TILAppAcronyms.

Modifying Dockerfiles

Before you can run everything, you must change the Dockerfiles. Docker Compose starts the different containers in the requested order but won’t wait for them to be ready to accept connections. This causes issues if your Vapor application tries to connect to a database before the database is ready. In TILAppAcronyms, open web.Dockerfile. Replace ENTRYPOINT ./Run serve --env $ENVIRONMENT --hostname 0.0.0.0 --port $PORT with the following:

ENTRYPOINT sleep 10 && \
  ./Run serve --env $ENVIRONMENT --hostname 0.0.0.0 --port $PORT

This tells the container to wait for 10 seconds before starting the Vapor application. This should give the databases enough time to start up. In a real application, you may want to consider putting this in a script and testing the database before starting the Vapor app. You can also see Chapter 33, “Deploying with Docker”, for a more robust solution.

In TILAppUsers, open web.Dockerfile and make the same change.

Running everything

You’re now ready to spin up your application in Docker Compose. In Terminal, in the directory containing docker-compose.yml, enter the following:

docker-compose up

This will download and build all the containers specified in docker-compose.yml and start them up. Note that it can take some time to build all the microservices.

When everything is up and running you’ll see something like:

You can then open RESTed and make requests like before.

Where to go from here?

In this chapter, you learned how to use Vapor to create an API gateway. This makes it simple for clients to interact with your different microservices. You learned how to send requests between different microservices and return single responses. You also learned how to use Docker Compose to build and start all the microservices and link them together.

You should now have the basic knowledge required to write powerful microservices. You can enhance this further with message queues, protocol buffers and remote procedural calls. There’s no limit to the applications you can now build!

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.