gRPC and Server Side Swift: Getting Started
Learn how to define an API with gRPC and how to integrate it in a Vapor application. By Walter Tyree.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
gRPC and Server Side Swift: Getting Started
35 mins
- Getting Started
- Installing Evans
- Installing Protoc and the Swift Plugins
- Adding gRPC Support to Vapor
- Learning About gRPC
- How is gRPC Different from REST?
- Learning the Difference Between gRPC and JSON?
- Working with a .proto File
- Defining the Services
- Defining the Messages
- Exercising a .proto File With Evans
- Generating Swift Code From a .proto file
- Messages Become Structs
- Working with Generated Code
- Turning Services Into Functions
- Implementing Service Functions
- Replacing the HTTP Server in Vapor
- Configuring the gRPC Server
- Using the gRPC Server
- Trying out the App
- Changing the API
- Updating the Spec
- Adding the Completion Logic
- Updating the Database Model
- Where to Go from Here?
Messages Become Structs
Open the todo.pb.swift file. In addition to generating Swift versions of properties of a Todo
, the new code also has a convenience method to work with the optional todoID
property, as displayed in the excerpt below.
struct Todos_Todo {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
var todoID: String {
get {return _todoID ?? String()}
set {_todoID = newValue}
}
/// Returns true if `todoID` has been explicitly set.
var hasTodoID: Bool {return self._todoID != nil}
/// Clears the value of `todoID`. Subsequent reads from it will
/// return its default value.
mutating func clearTodoID() {self._todoID = nil}
var title: String = String()
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
fileprivate var _todoID: String? = nil
}
In addition to the clearTodoID()
convenience function, the comment indicates that there are many more convenience methods for Message
structs. The best way to explore them is to open the swift-protobuf package and read the comments in the Messages.swift and the Messages+*.swift files. There you’ll find a number of convenience methods for working with JSON, text and data objects.
Notice how gRPC combines the package name with the message name when creating the Swift structure. This naming convention occurs throughout all of your gRPC code. Though it looks a little out of place in Swift code, it does help you remember when you’re working with the gRPC objects versus any other objects.
Working with Generated Code
If you’ve worked with other code generators before, you’ll be familiar with the pattern of using extensions and type aliases to separate your code from the generated, and often overwritten, code. For the sake of code organization, open Sources/App/Models/Todo.swift and replace //TODO: Create initializer methods
with the following:
extension Todos_Todo {
init (_ todo: Todo) {
if let todoid = todo.id {
self.todoID = todoid.uuidString
}
self.title = todo.title
}
}
This will help you to work directly with the Fluent model. Since the id
property is optional, you don’t want to set it unless your object already has one.
Now, add an extension to initialize a Todo
with a Todos_Todo
. At the end of the Todo.swift file, add the following code:
extension Todo {
convenience init (_ todo: Todos_Todo) {
self.init(id: UUID(uuidString: todo.todoID), title: todo.title)
}
}
Having these two initializers allows you to convert your Todo
models between the form that the PostgreSQL server wants and the form that gRPC wants.
Build the app again to make sure everything is still correct. The command is the same you have already used:
swift build
With the data model in place, now it’s time to check out the services.
Turning Services Into Functions
Now, open Sources/App/Controllers/todo.grpc.swift and find the section that defines the protocol Todos_TodoServiceProvider
. The spec in the proto file has been turned into Swift code, and the comments have also carried over!
By default, protoc generated client and server code in this file. There are options to generate only client or server code.
To make your server functional, you need to create a class that implements this Todos_TodoServiceProvider
protocol.
In the Controllers folder, add a new file called TodoProvider.swift. Calling something a ‘Provider’ isn’t really common in Swift, but it’s the convention when working with gRPC.
In your new file, add the following imports:
import Foundation
import GRPC
import Vapor
import Fluent
Next, add the following class:
class TodoProvider: Todos_TodoServiceProvider {
var interceptors: Todos_TodoServiceServerInterceptorFactoryProtocol?
//TODO: Add reference to Vapor
func fetchTodos(request: Todos_Empty, context: StatusOnlyCallContext)
-> EventLoopFuture<Todos_TodoList> {
//TODO: Add fetchTodos
}
func createTodo(request: Todos_Todo, context: StatusOnlyCallContext)
-> EventLoopFuture<Todos_Todo> {
//TODO: Add createTodo
}
func deleteTodo(request: Todos_TodoID, context: StatusOnlyCallContext)
-> EventLoopFuture<Todos_Empty> {
//TODO: Add deleteTodo
}
}
The name, request type and return type are all recognizable from the .proto file. In addition, there’s a context
parameter, which provides you with a logger
, the event loop for the call as well as the ability to set a response status and some other properties.
Next, you’ll write functions for the service provider.
Implementing Service Functions
Before you start adding code to the functions, add a reference to the Vapor app itself so you can work with the database. Replace //TODO: Add reference to Vapor
with:
var app: Application
init(_ app: Application) {
self.app = app
}
This code adds a variable to hold a reference to the Vapor app and adds an init method so you can pass in the app when everything starts up.
The next step is to add the code for each of the functions. You’ll query the database for each function and return data or an error.
Replace //TODO: Add fetchTodos
with:
let todos = Todo.query(on: app.db(.psql)).all()
.map { todos -> Todos_TodoList in //1
var listToReturn = Todos_TodoList()
for td in todos {
listToReturn.todos.append(Todos_Todo(td)) //2
}
return listToReturn
}
return todos //3
Here’s what this code is doing:
- Queries for all the TODO records and passes the results into a
.map
. - Loops through the TODO records and converts each one into a gRPC
Todos_Todo
object using the initializer you created earlier. - Returns the array of gRPC TODOs.
Now replace //TODO: Add createTodo
with:
let todo = Todo(request) //1
return todo.save(on: app.db(.psql)).map { //2
Todos_Todo(todo) //3
}
Here’s what this code is doing:
- Converts the gRPC
Todos_Todo
in the request to aTodo
to use with PostreSQL. - Saves the new
todo
to the database. - Converts the
todo
into the gRPC format and sends it back to the client.
The function to delete is a little longer because it validates the request and needs to find the corresponding entry in the database before it can delete it. Replace //TODO: Add deleteTodo
with the following:
guard let uuid = UUID(uuidString: request.todoID) else { //1
return context.eventLoop.makeFailedFuture(
GRPCStatus(code: .invalidArgument, message: "Invalid TodoID")) //2
}
return Todo.find(uuid, on: app.db(.psql)).unwrap(or: Abort(.notFound))
.flatMap { [self] todo in //3
todo.delete(on: app.db(.psql))
.transform(to: context.eventLoop.makeSucceededFuture(Todos_Empty())) //4
}
Here’s what this code is doing:
- Attempts to make a
UUID
out of the.todoID
property of therequest
. - If a
UUID
can’t be made, creates a failed future to pass back with a gRPC error object. - Finds the
Todo
record in the database using theuuid
. - Deletes the record and returns a succeeded future.
Whew, that’s a lot of code. Run the build command again to make sure everything still builds with no issues:
swift build
The expected output is something like this:
starter> swift build
[5/5] Build complete!
starter>
Now it’s time to integrate gRPC in the existing Vapor app.