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

35. Microservices, Part 1
Written by Tim Condon

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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

In previous chapters, you’ve built a single Vapor application to run your server code. For large applications, the single monolith becomes difficult to maintain and scale. In this chapter, you’ll learn how to leverage microservices to split up your code into different applications. You’ll learn the benefits and the downsides of microservices and how to interact with them. Finally, you’ll learn how authentication and relationships work in a microservices architecture.

Microservices

Microservices are a design pattern that’s become popular in recent years. The aim of microservices is to provide small, independent modules that interact with one another. This is different to a large monolithic application. This makes the individual services easier to develop and test as they are smaller. Because they are independent, you can develop them individually. This removes the need to use and build all the dependencies for the entire application.

Microservices also allow you to scale your application better. In a monolithic application, you must scale the entire application when under heavy load. This includes parts of the application that receive low traffic. In microservices, you scale only the services that are busy.

Finally, microservices make building and deploying your applications easier. Deploying very large applications is complex and prone to errors. In large applications, you must coordinate with every development team to ensure the application is ready to deploy. Breaking a monolithic application up into smaller services makes deploying each service easier.

Each microservice should be a fully contained application. Each service has its own database, its own cache and, if necessary, its own front end. The only shared part should be the public API to allow other services to interact with that microservice. Typically, they provide an HTTP REST API, although you can use other techniques such as protobuf or remote procedural calls (RPC). Since each microservice interacts with other services only via a public API, each can use different technology stacks. For instance, you could use PostgreSQL for one service that required it, but use MySQL for the main user service. You can even mix languages. This allows different teams to use the languages they prefer.

Swift is an excellent choice for microservices. Swift applications have low memory footprints and can handle large numbers of connections. This allows Swift microservices to fit easily into existing applications without the need for lots of resources.

The TIL microservices

In the first few sections of this book, you developed a single TIL application. You could have used a microservices architecture instead. For instance, you could have one service that deals with users, another that deals with categories and another for acronyms. Throughout this chapter, you’ll start to see how to do this.

The user microservice

Navigate to the TILAppUsers directory in Terminal. Enter the following the start the database:

docker run --name postgres -e POSTGRES_DB=vapor \
  -e POSTGRES_USER=vapor -e POSTGRES_PASSWORD=password \
  -p 5432:5432 -d postgres
vapor xcode -y

The acronym microservice

Keep the user service running and navigate to the TILAppAcronyms directory in Terminal. Enter the following the start the database:

docker run --name mysql -e MYSQL_USER=vapor \
  -e MYSQL_PASSWORD=password -e MYSQL_DATABASE=vapor \
  -p 3306:3306 -d mysql/mysql-server:5.7
vapor xcode -y

Dealing with relationships

At this point you can create both users and acronyms in their respective microservices. However, dealing with relationships between different services is more complicated. In Section 1 of this book, you learned how to use Fluent to enable you to query for different relationships between models. With microservices, since the models are in different databases, you must do this manually.

Getting a user’s acronyms

In the TILAppAcronyms Xcode project, open AcronymsController.swift. Below updateHandler(_:) add a new route handler to get the acronyms for a particular user:

func getUsersAcronyms(_ req: Request)
  throws -> Future<[Acronym]> {
    // 1
    let userID = try req.parameters.next(UUID.self)
    // 2
    return Acronym.query(on: req)
      .filter(\.userID == userID)
      .all()
}
router.get("user", UUID.parameter, use: getUsersAcronyms)

Getting an acronym’s user

You can already get an acronym’s user with the current projects. You make a request to get the acronym, extract the user’s ID from it, then make a request to get the user from the user service. Chapter 37, “Microservices, Part 2” discusses how to simplify this for clients.

Authentication in Microservices

Currently a user can create, edit and delete acronyms with no authentication. Like the TIL app, you should add authentication to microservices as necessary. For this chapter, you’ll add authentication to the TILAppAcronyms microservice. However, you’ll delegate this authentication to the TILAppUsers microservice.

Logging in

Open the TILAppUsers project in Xcode. The starter project already contains a Token type and an empty AuthContoller. You could store the tokens in the same database as the user. Since every validation request requires a lookup and you have multiple services, you want this to be as quick as possible. One solution is to store them in memory. However, if you want to scale your microservice, this doesn’t work. You need to use something like Redis. Redis is a fast, key-value database, which is ideal for storing session tokens. You can share the database across different servers which allows you to scale without any performance penalties.

docker run --name redis -p 6379:6379 -d redis
import Redis
// 1
var redisConfig = RedisClientConfig()
// 2
if let redisHostname = Environment.get("REDIS_HOSTNAME") {
  redisConfig.hostname = redisHostname
}
// 3
let redis = try RedisDatabase(config: redisConfig)
// 4
databases.add(database: redis, as: .redis)
func loginHandler(_ req: Request) throws -> Future<Token> {
  // 1
  let user = try req.requireAuthenticated(User.self)
  // 2
  let token = try Token.generate(for: user)
  // 3
  return req.withPooledConnection(to: .redis) { redis in
    // 4
    redis.jsonSet(token.tokenString, to: token)
      .transform(to: token)
  }
}
// 1
let authGroup = router.grouped("auth")
// 2
let basicAuthMiddleware =
  User.basicAuthMiddleware(using: BCryptDigest())
// 3
let basicAuthGroup = authGroup.grouped(basicAuthMiddleware)
// 4
basicAuthGroup.post("login", use: loginHandler)

Authenticating tokens

First, create a new type to represent the data sent in token validation requests. At the bottom of AuthController.swift add the following:

struct AuthenticateData: Content {
  let token: String
}
func authenticate(_ req: Request, data: AuthenticateData)
  throws -> Future<User.Public> {
    // 1
    return req.withPooledConnection(to: .redis) { redis in
      // 2
      return redis.jsonGet(data.token, as: Token.self)
        .flatMap(to: User.Public.self) { token in
          // 3
          guard let token = token else {
            throw Abort(.unauthorized)
          }
          // 4
          return User.query(on: req)
            .filter(\.id == token.userID)
            .first()
            .unwrap(or: Abort(.internalServerError))
            .convertToPublic()
        }
    }
}
authGroup.post(
  AuthenticateData.self,
  at: "authenticate",
  use: authenticate)

Authenticating with other microservices

Go back to the TILAppAcronyms project in Xcode and stop the app and close Xcode. Navigate to the project directory in Terminal and enter the following commands:

touch Sources/App/Middlewares/UserAuthMiddleware.swift
vapor xcode -y
import Authentication
extension User: Authenticatable {}
import Vapor

struct AuthenticateData: Content {
  let token: String
}
final class UserAuthMiddleware: Middleware {
  // 1
  func respond(to request: Request, chainingTo next: Responder)
    throws -> Future<Response> {
      // 2
      guard let token =
        request.http.headers.bearerAuthorization else {
          throw Abort(.unauthorized)
        }

      // 3
      return try request
        .client()
        .post("http://localhost:8081/auth/authenticate") {
        authRequest in
          // 4
          try authRequest.content
            .encode(AuthenticateData(token: token.token))
          }.flatMap(to: Response.self) { response in
            // 5
            guard response.http.status == .ok else {
              if response.http.status == .unauthorized {
                throw Abort(.unauthorized)
              } else {
                throw Abort(.internalServerError)
              }
            }
            // 6
            let user =
              try response.content.syncDecode(User.self)
            // 7
            try request.authenticate(user)
            // 8
            return try next.respond(to: request)
          }
    }
}
let authGroup = router.grouped(UserAuthMiddleware())
authGroup.post(use: createHandler)
authGroup.delete(Acronym.parameter, use: deleteHandler)
authGroup.put(Acronym.parameter, use: updateHandler)
router.post(use: createHandler)
router.delete(Acronym.parameter, use: deleteHandler)
router.put(Acronym.parameter, use: updateHandler)
struct AcronymData: Content {
  let short: String
  let long: String
}
// 1
let data = try req.content.syncDecode(AcronymData.self)
// 2
let user = try req.requireAuthenticated(User.self)
// 3
let acronym = Acronym(
  short: data.short,
  long: data.long,
  userID: user.id)
return acronym.save(on: req)
return try flatMap(
  to: Acronym.self,
  req.parameters.next(Acronym.self),
  req.content.decode(AcronymData.self)) { acronym, updateData in
let user = try req.requireAuthenticated(User.self)
acronym.userID = user.id

Where to go from here?

In this chapter, you learned how to split the TIL app into different microservices for users and acronyms. You’ve seen how to handle authentication and relationships across different services.

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.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now