How to build a GraphQL server with Swift and Vapor
GraphQL is a query language for APIs developed by Facebook but open sourced in 2015. It is an alternative to REST based APIs because it allows clients to decide which data they need instead of the server upfront deciding which data is delivered on each endpoint. It is a modern way of querying data, for example some big companies next to Facebook like GitHub or Shopify have seen the advantages and implemented their APIs with GraphQL.
If you want to learn more about GraphQL go and check out their website. They provide great articles and tutorials to get started.
At the end of this article we'll have a running GraphQL server that we can reach via the web-based GraphQL query tool GraphiQL. As an example we use a todo app. We are going to create new todos, list all existing once and also delete todos.
If you already have some experience with Vapor and have created a new application before, you will recognize the todo template, that comes with every new Vapor application, in this article. We are going to build the same functionality with GraphQL.
We're using Swift 5.3 with Vapor 4.
This Tutorial has been updated for Vapor 4 and the usage of Result Builders
Main part
Getting started
Let's start by creating a new Vapor project. The easiest way to do that is using the toolbox which provides the command vapor new GraphQL-Todo
. This creates a new Swift Package Manager application with some default configuration for Vapor. We'll use this as a base to build our GraphQL server on top of that.
First we need to modify our Package.swift
file and add new dependencies. The GraphQLKit
package provides a thin wrapper around the core functions to make them easier to use with Vapor
. It also includes the GraphQL implementation for Swift itself, Graphiti
and GraphQL
. We don't need to import both packages directly as GraphQLKit
imports and exposes them automatically. The second new dependency is optional but provides us a GraphiQL web page we can serve from our web server that gives as an IDE-like interface to send queries against our GraphQL server.
// swift-tools-version:5.1
import PackageDescription
let package = Package(
name: "GraphQL-Todo",
products: [
.library(name: "GraphQL-Todo", targets: ["App"]),
],
dependencies: [
// 💧 A server-side Swift web framework.
.package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"),
// 🔵 Swift ORM (queries, models, relations, etc) built on SQLite 3.
.package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"),
.package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.0.0"),
// 🌐 GraphQL
.package(url: "https://github.com/alexsteinerde/graphql-kit.git", from: "2.0.0"), // Vapor Utilities
.package(url: "https://github.com/alexsteinerde/graphiql-vapor.git", from: "2.0.0"), // Web Query Page
],
targets: [
.target(name: "App", dependencies: ["Fluent", "FluentSQLiteDriver", "Vapor", "GraphQLKit", "GraphiQLVapor"])
.target(name: "Run", dependencies: ["App"]),
.testTarget(name: "AppTests", dependencies: ["App"])
]
)
Database Model
We'll use the existing Todo
database model that is provided by the template. Nothing needs to be changed here to support GraphQL:
import Fluent
import Vapor
/// A single entry of a Todo list.
final class Todo: Model {
static let schema = "todos"
/// The unique identifier for this `Todo`.
@ID(key: .id)
var id: UUID?
/// A title describing what this `Todo` entails.
@Field(key: "title")
var title: String
init() { }
/// Creates a new `Todo`.
init(id: UUID? = nil, title: String) {
self.id = id
self.title = title
}
}
struct MigrateTodos: Migration {
func prepare(on database: Database) -> EventLoopFuture<Void> {
return database.schema("todos")
.id()
.field("title", .string, .required)
.create()
}
func revert(on database: Database) -> EventLoopFuture<Void> {
return database.schema("todos").delete()
}
}
Business Logic
The REST template provides the following 3 endpoints:
- Get all todos as a list
- Create a new todo based on query input
- Delete an existing todo based on the todo identifier.
We will recreate the same endpoints in GraphQL. Therefore we create a class called TodoResolver
that holds the business and database logic for the endpoints. This gives us a nice abstraction of logic.
First we will write the function that returns all Todo
s that are stored in the database.
final class TodoResolver {
func getAllTodos(request: Request, _: NoArguments) throws -> EventLoopFuture<[Todo]> {
Todo.query(on: request.db).all()
}
}
We have the function getAllTodos
. It takes a Request
object and to make it conform to later GraphQL functionality we also accept an argument of type NoArguments
. This type comes with the Graphiti
package and is just a typealias for an empty struct. We will discover how we can use other types of arguments in the next APIs. The rest of this function is normal Vapor related code. This function returns a future with an array of Todo
objects. The function body contains the query we make with Fluent. And because we have the Request
object given as a function parameter we can use it as an event loop for our database query.
Since Swift 5.1 we can omit the return keyword here because the function consists only of a single line statement.
extension TodoResolver {
struct CreateTodoArguments: Codable {
let title: String
}
func createTodo(request: Request, arguments: CreateTodoArguments) throws -> EventLoopFuture<Todo> {
let todo = Todo(title: arguments.title)
return todo.create(on: request.db).map { todo }
}
}
Now we start using an explicit argument type to create a new todo. We want to be able to create a new todo based on just a title
. To make this work with the GraphQL implementation we create a new struct that conforms to Codable
and add all parameters we want to accept (in this case just the title
attribute but this list can be longer). In the function we expect this CreateTodoArguments
type as the second argument instead of the NoArguments
type from our previous function. Then we initialize our Todo
object with the passed title argument and save it to the database by using our request object.
Finally we also want to delete existing todos based on their identifiera. We create the arguments struct for this function called DeleteTodoArguments
with the property id
. The deleteTodo
function expects this type as the second argument. This function returns a future of type Bool
to indicate that the deletion was successful. To delete the todo we use Fluents functionality. First we search for the Todo
with the given id. If none was found we throw an .notFound
error which results in a Not-Found status message at the endpoint. Then we delete that todo and if it doesn't throw an error we transform the result to a boolean value of true
which indicate a successful delete progress.
extension TodoResolver {
struct DeleteTodoArguments: Codable {
let id: UUID
}
func deleteTodo(request: Request, arguments: DeleteTodoArguments) throws -> EventLoopFuture<Bool> {
Todo.find(arguments.id, on: request.db)
.unwrap(or: Abort(.notFound))
.flatMap({ $0.delete(on: request.db) })
.transform(to: true)
}
}
GraphQL Schema
The only thing that is still missing is our GraphQL schema. It is a structure of all types we want to expose and endpoints that can be called. Endpoints are split into fetching endpoints which are grouped as queries and mutable endpoints that can modify data and are grouped as mutations.
If you want to know more about Queries and Mutation, I recommend the official GraphQL documentation.
For our todo example we need to define our Todo
class, the query to get all todos and two mutations to create new todos and delete existing once. The Schema
type is based on two generics. One is the API which we created as TodoResolver
and one is the context which is the Request
object in our application.
Why can't the compiler infer all fields of our ToDo type from the given Swift class? That's because we could also have computed values that come from other sources or functions that work as properties and can also be added to types.
import Foundation
import Graphiti
import Vapor
// Definition of our GraphQL schema.
let todoSchema = try! Schema<TodoResolver, Request> {
Scalar(UUID.self)
// Todo type with it's fields
Type(Todo.self) {
Field("id", at: \.id)
Field("title", at: \.title)
}
// We only have one single query: Getting all existing todos
Query {
Field("todos", at: TodoResolver.getAllTodos)
}
// Both mutations accept arguments.
// First we define the name
// and we pass the keypath to the field of the argument struct.
Mutation {
Field("createTodo", at: TodoResolver.createTodo) {
Argument("title", at: \.title)
}
Field("deleteTodo", at: TodoResolver.deleteTodo) {
Argument("id", at: \.id)
}
}
}
Register Schema on Vapor router
To serve our GraphQL schema as a REST endpoint we use the GraphQLKit functions to easily register the schema together with an instance of our TodoResolver
on our router. What it does is creating a GET and a POST endpoint at /graphql
. This is the default path for a REST based GraphQL server. While we are in a development environment we also enable the GraphiQL web page to debug our application easily with a built in IDE-like endpoint.
// Register the schema and its resolver.
app.register(graphQLSchema: todoSchema, withResolver: TodoResolver())
// Enable GraphiQL web page to send queries to the GraphQL endpoint
if !app.environment.isRelease {
app.enableGraphiQL()
}
Testing our API
We can run the Vapor application now and then visit http://localhost:8080. We will see a dashboard where we can enter queries on the left-hand side and see results in the middle. On the right we can find a full documentation of our API that we exposed with our Schema
object.
Here are examples of queries we can send to our API and expect results back:
# Query
# Quering all todos with it's ids and titles.
query GetAllTodos {
todos {
id
title
}
}
# But we can also just query ids and it will only send them wihtout the title data.
query GetTodoIDs {
todos {
id
}
}
# We can include arguments directly into the query
mutation createTodo {
createTodo(title: "Cleanup the house") {
id
title
}
}
# Or we can also define parameters that are fetched from the JSON formatted parameters field below.
mutation deleteTodo($id: UUID!) {
deleteTodo(id: $id)
}
# Parameters
{
"id": "1b06a7bc-b542-41cf-8b2a-328943a5ce80"
}
// Response when creating a new todo via the mutation
{
"data": {
"createTodo": {
"id": "1b06a7bc-b542-41cf-8b2a-328943a5ce80",
"title": "Cleanup the house"
}
}
}
Conclusion
GraphQL is a great query language that can also be used with Swift and the great ecosystem Vapor provides. Some functionalities like subscriptions with websockets are not implemented yet but this will probably come in the future too.
This example demonstrates the type safe syntax Swift provides combined with the flexibility in querying data GraphQL has. All code you have seen in this article can be found in a GitHub project I created. This project can be used the same way the Vapor API template is used for new applications.
What experiences do you have with GraphQL and Server Side Swift? In which areas do you think can the community help to provide a better experience creating GraphQL applications in Swift? Let me know your thoughts on Twitter or via email.