GraphQL Tutorial for Server-Side Swift with Vapor: Getting Started

For a long time, solving the problem of API integrations between frontend and server-side seemed trivial. You might have stumbled across some HTML form encoding or legacy APIs that relied on SOAP and XML, but most APIs used REST with JSON encoding. While REST looked like the de-facto standard, ironically, it didn’t have a defined […] By Max Desiatov.

Leave a rating/review
Download materials
Save for later
Share

For a long time, solving the problem of API integrations between frontend and server-side seemed trivial. You might have stumbled across some HTML form encoding or legacy APIs that relied on SOAP and XML, but most APIs used REST with JSON encoding.

While REST looked like the de-facto standard, ironically, it didn’t have a defined spec to adhere to. Additionally, common REST patterns don’t work in all situations. Developers needed a clearly defined API specification that multiple libraries in different ecosystems could conform to.

There were multiple attempts to create this specification, but GraphQL gained the most traction. Many public API providers, including GitHub, Shopify, Facebook, Pinterest and Airbnb, adopted GraphQL APIs. With the first spec introduced in 2015, GraphQL has gained client and server implementations for all of the most popular languages, including Swift.

In this tutorial, you’ll get started with GraphQL and Server-Side Swift using a concrete API as an example. With so many TV shows out there, how do you pick the best one without checking reviews first? An API providing a list of available shows and reviews via GraphQL would be a great learning opportunity.

In this tutorial, you’ll build an API server called Fresh Tomatoes. You’ll use Vapor, the most popular server-side Swift framework, and the GraphQLKit library for the API. Along the way, you’ll learn:

  • How GraphQL compares to REST.
  • What fields, queries and mutations are in GraphQL.
  • How to expose your model types and parent-child relationships in a GraphQL API.
  • Pagination in GraphQL.

Getting Started

Download the project materials by clicking the Download Materials button at the top of or bottom of this page. Unpack the downloaded archive, and navigate to starter. Open Package.swift with Xcode and wait until all items in Swift Package Dependencies finish loading:

The starter project opened in Xcode

Click Run in the top-left corner of the screen, as highlighted on the screenshot above. After the build process finishes, you’ll see this message at the bottom of the window in your Xcode console:

[ NOTICE ] Server starting on http://127.0.0.1:8080

Open your favorite browser and navigate to http://127.0.0.1:8080, which will look similar to the following:

The GraphiQL client page in opened in the browser

What you see in the browser is the de-facto standard GraphQL client, GraphiQL. You can specify your GraphQL requests in the editor on the left side. The view on the right displays a corresponding response.

The "error": true response you see on the right is normal for a starter project. You haven’t defined your GraphQL schema yet, so there’s nothing for the server to return, even for an empty request.

You can use any other HTTP client to issue raw GraphQL requests, but you’ll probably find GraphiQL one of the most convenient during development. You’ll learn how to use the cURL HTTP client in a future section when you have a meaningful schema to query.

Before you jump into coding, let’s take a moment to explore how GraphQL differs from REST.

How GraphQL Differs From REST

The fact that REST doesn’t have a defined standard or specification is a significant downside. This leaves API authors with many decisions to make and no way to enforce inconsistencies in numerous implementations. Here are a few questions you may have when developing with REST:

  • How do you implement validation and communicate validation errors to the client?
  • Do pagination and filtering work? If so, how?
  • How do you document and test your API?
  • Is there a way to represent relationships and batch queries?

If you develop server and client code separately, you have to figure out the communication processes, too.

  • How does the server-side team communicate about changes to the existing API?
  • What happens after you deprecate an API or when validation rules change?
  • How do you make sure you don’t forget to reflect these changes in every client app?

Over the years, some solutions emerged within the REST school of thought. At the moment, OpenAPI is the most prominent.

In contrast, GraphQL’s authors tried to avoid these issues from the beginning.

You start developing a GraphQL API with a schema that describes model types and their fields. Usually, you specify it in a specialized but simple GraphQL schema language. Both server and client code interact with each other only using types and fields declared in the schema.

The schema language also supports user-defined types and even documentation comments. With GraphQL’s clear specification, you get an integrated experience with libraries and developer tools that conform to it. You can then store your API schema in a code repository, letting you easily track any changes, as you would with any other code.

Both client and server apps can reuse it, while developer tools can provide advanced features such as code generation, autocomplete and documentation renderers. When you update the schema on the server, your client code automatically validates existing requests against the new schema.

Overall, this lets you iterate faster on both sides of the stack. What’s not to like?

With that out of the way, it’s time to start coding, and where better to start than your models.

Declaring Model Types

Before you start writing your GraphQL schema, you need to add a few simple model types to the server. In the project’s App directory, create a new subdirectory called Models. In this new subdirectory, create a new file named Show.swift and add the following:

import Fluent
import Vapor

final class Show: Model, Content {
  static let schema = "shows"

  @ID(key: .id)
  var id: UUID?

  @Field(key: "title")
  var title: String

  @Field(key: "releaseYear")
  var releaseYear: Int

  init() { }

  init(id: UUID? = nil, title: String, releaseYear: Int) {
    self.id = id
    self.title = title
    self.releaseYear = releaseYear
  }
}

Here you create a Show type that conforms to Model. Your new type declares three fields, of which id with the @ID property wrapper is the most important. The database uses this field to uniquely identify a model instance.

The rest of the fields use @Field, which takes a database key string as an argument. For consistency, you specify the database key string to be the same names of your corresponding properties. You also declare two initializers: One creates a fresh empty model instance, and the other is for an instance with a specified title and release year.

Next, you need to tell the database about your new model. Underneath, the App directory, create Migrations as a sibling of Models. Add a new file named MigrateShows.swift with this code:

import Fluent

struct MigrateShows: Migration {
  func prepare(on database: Database) -> EventLoopFuture<Void> {
    return database.schema("shows")
      .id()
      .field("title", .string, .required)
      .field("releaseYear", .int, .required)
      .create()
  }

  func revert(on database: Database) -> EventLoopFuture<Void> {
    return database.schema("reviews").delete()
  }
}

Here, prepare specifies appropriate types for each field and creates a new database schema with the name shows. revert runs when you need to revert this migration. In this case, deleting the previously created schema is enough.

The database schema and GraphQL schema are separate entities. The former describes how your server stores data, while the latter describes how it communicates with its clients.

Next, open the project’s existing configure.swift and the new migration just above try app.autoMigrate().wait() in configure:

app.migrations.add(MigrateShows())
Note: In this tutorial, you use an in-memory SQLite database, so data won’t persist between runs. That said, feel free to use any other database driver supported by Fluent.

Here’s what your project structure looks like after adding the new files:

The file tree of the project after the MigrateShows.swift file is added

Build the project to verify that new models and migrations compile without any errors.

Next, you’ll take a look at GraphQL fields, queries and mutations.

GraphQL Fields, Queries and Mutations

After declaring the model types for your server’s database, you still have to define a GraphQL schema. Any GraphQL schema contains at least a few types that roughly correspond to your database types. Here’s a declaration in the GraphQL schema language for Show:

type Show {
  id: ID
  title: String!
  releaseYear: Int!
}

The declaration above introduces a new type with the type keyword. It also specifies three fields that take no arguments and return values of corresponding types. Fields without arguments almost read like property declarations in Swift, but without the let keyword.

GraphQL also supports optional values. While in Swift you have to explicitly specify a type is optional with a question mark, in GraphQL you explicitly specify a type is non-optional with an exclamation mark. Thus, in this notation ID is optional, while String! is not.

GraphQL makes a distinction between two types of requests: queries and mutations. Queries are read-only requests that aren’t supposed to modify anything on a server. In contrast, mutations mutate the state of the server by definition.

These requests have actual types that specify queries and mutations as their fields. Here’s how it looks:

type Query {
  shows: [Show!]!
}

type Mutation {
  createShow(title: String! releaseYear: Int!): Show!
  deleteShow(id: UUID!): Boolean!
  updateShow(id: UUID! releaseYear: Int! title: String!): Boolean!
}

This reads as a single shows query that returns a non-optional array of non-optional Show values. At the same time, this schema declares three mutations, each taking non-optional arguments. The createShow mutation returns a newly created non-optional Show value, while deleteShow and updateShow return a non-optional boolean value that indicates whether a mutation succeeded.

With that out of the way, you can move on to defining GraphQL queries.

Defining GraphQL Queries

In this tutorial, instead of using the GraphQL schema language directly, you’ll rely on a domain-specific language from the Graphiti library. This lets you declare fields, queries and mutations in Swift in a type-safe way. Actual code that returns data for a given field is a resolver, which you’ll declare on a new Resolver class.

In App, create a new subdirectory called GraphQL. Next, create new file named Resolver.swift and replace its contents with the following:

import Graphiti
import Vapor

final class Resolver {
  func getAllShows(
    request: Request,
    arguments: NoArguments
  ) throws -> EventLoopFuture<[Show]> {
    Show.query(on: request.db).all()
  }
}

Here’s a code breakdown:

  1. The new getAllShows serves a response to an instance of Request, which Vapor declares.
  2. db lets you access the database, and the static Show.query with all() on top of it fetches all shows.
  3. The second arguments parameter of type NoArguments from Graphiti explicitly indicates that this field takes no arguments.

Now, in GraphQL, create a file named Schema.swift. Add this schema definition:

import Graphiti
import Vapor

let schema = try! Schema<Resolver, Request> {
  Scalar(UUID.self)

  Type(Show.self) {
    Field("id", at: \.id)
    Field("title", at: \.title)
    Field("releaseYear", at: \.releaseYear)
  }

  Query {
    Field("shows", at: Resolver.getAllShows)
  }
}
Note: Since UUID is not a built-in type in GraphQL, you have to declare it separately in the schema as a Scalar. The Type and Query blocks declare the Show type and the shows query respectively.

Great! You defined your first type and query in a GraphQL Schema using Server-side Swift. Remember, this schema serves as a blueprint for your API. Any apps consuming the API can essentially share the schema, so your frontend and backend are on the same page.

Next, go to configure.swift and add this line right above the GraphiQL setup code, but below the migration code:

app.register(graphQLSchema: schema, withResolver: Resolver())

This line registers the GraphQL schema with your application, using your Resolver to allow access to Show‘s queries.

Next, you’ll explore the GraphQL API.

Exploring the GraphQL API

You’re going to explore your GraphQL schema using your browser. Build and run the project to see your first working GraphQL schema. Refresh the browser tab pointing to http://127.0.0.1:8080 and type your first query in the query editor:

query {
  shows {
    id
    title
  }
}

Press Cmd+Enter or click the button with the Run triangle at the top left corner of GraphiQL UI. You’ll see an empty result to the right, which is to be expected from an empty database.

The GraphiQL client page with no results in an empty database for the shows query

Notice the Docs navigation button in the top right corner highlighted with a red oval on the screenshot above. Clicking it displays interactive documentation for your schema, which you’ll find quite handy when working with any non-trivial GraphQL API.

You can verify your API in terminal with the cURL HTTP client like this:

curl -X "POST" "http://127.0.0.1:8080/graphql" -H 'Content-Type: application/json' -d $'{
  "query": "query { shows { id title } }",
  "variables": {}
}'

Here you issue a simple POST request to http://127.0.0.1:8080/graphql, which is the entry point for the API. The request body contains a JSON object with only two fields: query with the query body and variables for parameterized queries or mutations that you’ll learn about later.

Now you’ll learn how to define GraphQL mutations.

Defining GraphQL Mutations

To fill the database with data you need to define appropriate mutations in the schema. Open Resolver.swift and add these struct declarations and functions to the body of Resolver:

struct CreateShowArguments: Codable {
  let title: String
  let releaseYear: Int
}

func createShow(
  request: Request,
  arguments: CreateShowArguments
) throws -> EventLoopFuture<Show> {
  let show = Show(
    title: arguments.title,
    releaseYear: arguments.releaseYear
  )
  return show.create(on: request.db).map { show }
}

struct UpdateShowArguments: Codable {
  let id: UUID
  let title: String
  let releaseYear: Int
}

func updateShow(
  request: Request,
  arguments: UpdateShowArguments
) throws -> EventLoopFuture<Bool> {
  Show.find(arguments.id, on: request.db)
    .unwrap(or: Abort(.notFound))
    .flatMap { (show: Show) -> EventLoopFuture<()> in
      show.title = arguments.title
      show.releaseYear = arguments.releaseYear
      return show.update(on: request.db)
    }
    .transform(to: true)
}

struct DeleteShowArguments: Codable {
  let id: UUID
}

func deleteShow(
  request: Request,
  arguments: DeleteShowArguments
) -> EventLoopFuture<Bool> {
  Show.find(arguments.id, on: request.db)
    .unwrap(or: Abort(.notFound))
    .flatMap { $0.delete(on: request.db) }
    .transform(to: true)
}

Each function here runs a database statement that takes arguments passed in a corresponding Codable structure. Check out the documentation in the Fluent module for more details about these database functions.

Next, open Schema.swift and add the following mutation definition below the Query:

Mutation {
  Field("createShow", at: Resolver.createShow) {
    Argument("title", at: \.title)
    Argument("releaseYear", at: \.releaseYear)
  }

  Field("updateShow", at: Resolver.updateShow) {
    Argument("id", at: \.id)
    Argument("title", at: \.title)
    Argument("releaseYear", at: \.releaseYear)
  }

  Field("deleteShow", at: Resolver.deleteShow) {
    Argument("id", at: \.id)
  }
}

Notice how this Mutation is different from the Query. Every Field here requires explicit arguments with their names. In addition, they need keypaths to properties of Arguments you’ve previously declared in the Resolver body.

Build and run. Then refresh the GraphiQL page. Now run createShow to add a new show:

mutation {
  createShow(
    title: "The Chilling Adventures of RESTful Heroes"
    releaseYear: 2020
  ) {
    id
  }
}

Now you’ll see the newly created shoe’s UUID in the response:

Results of the createShow mutation displayed in the GraphiQL client

Notice that the documentation explorer in the rightmost part of the window displays all of the newly defined mutations. This is one of the benefits of integrated GraphQL tooling: You get nicely formatted and highlighted API documentation regenerated in real-time as you go along. How’s about that, RESTful Heroes? :]

Make sure you also try updateShow and deleteShow, which, you guessed it right, update and delete existing shows.

Next, you’ll add pagination to the GraphQL schema.

Adding Pagination to the GraphQL Schema

Now that you can have multiple shows in your database and know how to handle GraphQL fields with arguments, it’s time to support pagination. Pagination lets you perform queries that return a limited number of results and keep track of where you left off, so you can request the next set.

Open Resolver.swift, and add this type declaration to the top of file above the definition for Resolver:

struct PaginationArguments: Codable {
  let limit: Int
  let offset: Int
}

Next, modify getAllShows in the body of Resolver to pass values of this new struct’s properties to limit and offset query modifiers like this:

func getAllShows(
  request: Request,
  arguments: PaginationArguments
) throws -> EventLoopFuture<[Show]> {
  Show.query(on: request.db)
    .limit(arguments.limit)
    .offset(arguments.offset)
    .all()
}

Now you have to update the GraphQL schema so it passes the new arguments to the resolver function. Navigate to Schema.swift and replace the Query in it with:

Query {
  Field("shows", at: Resolver.getAllShows) {
    Argument("limit", at: \.limit)
    Argument("offset", at: \.offset)
  }
}

Build and run. Then navigate to the GraphiQL client and click History in the top left panel:

The history navigator in the GraphiQL client

As the project currently uses an in-memory database, restarting the process will automatically clean it up. Going through history lets you quickly select a createShow mutation to populate the database again. Add a couple of shows to the database and run this query:

query {
  shows(limit: 10, offset: 0) {
    id
    title
  }
}

Verify that the shows you initially created display correctly. You can also play with different limit and offset arguments to see differently filtered pagination results.

The GraphiQL client with results of a query with pagination

Now it’s time to add a related model type.

Adding a Related Model Type

You’ve worked with shows, but what about actual reviews? Fear not! Adding a related model type is similar to what you’ve seen so far and only requires a special property wrapper to link the types.

In Models, create a new file named Review.swift and add the following:

import Fluent
import Vapor

final class Review: Model, Content {
  static let schema = "reviews"

  @ID(key: .id)
  var id: UUID?

  @Field(key: "title")
  var title: String

  @Field(key: "text")
  var text: String

  @Parent(key: "show_id")
  var show: Show

  init() { }

  init(id: UUID? = nil, showID: UUID, title: String, text: String) {
    self.id = id
    self.$show.id = showID
    self.title = title
    self.text = text
  }
}

Next, open Show.swift and add a new field to Show below the rest of the field:

@Children(for: \.$show)
var reviews: [Review]

The @Parent and @Children property wrappers define the corresponding fields of the Parent and Child types. With the Parent and Child models defined, your two models are now appropriately linked.

You also need to declare resolver functions to actually fetch these reviews in relation to a given show. Add this extension to the bottom of the Show.swift:

extension Show {
  func getReviews(
    request: Request,
    arguments: PaginationArguments
  ) throws -> EventLoopFuture<[Review]> {
    $reviews.query(on: request.db)
      .limit(arguments.limit)
      .offset(arguments.offset)
      .all()
  }
}

Then in Migrations, create a new file named MigrateReviews.swift and add the following:

import Fluent

struct MigrateReviews: Migration {
  func prepare(on database: Database) -> EventLoopFuture<Void> {
    return database.schema("reviews")
      .id()
      .field("title", .string, .required)
      .field("text", .string, .required)
      .field("show_id", .uuid, .required, .references("shows", "id"))
      .create()
  }

  func revert(on database: Database) -> EventLoopFuture<Void> {
    return database.schema("reviews").delete()
  }
}

This barely differs from the MigrateShows type you declared previously. The only significant difference is the .references("shows", "id") argument passed for the relation show_id field.

Next, open configure.swift and add the following migration to configure below the line migrating MigrateShows:

app.migrations.add(MigrateReviews())

Next, you’ll explore how to handle parent-child relationships in GraphQL.

Handling Parent-Child Relationships in GraphQL

The new Review type needs function definitions in Resolver.swift, too.

Add these CRUD functions at the bottom of Resolver‘s body, right below the existing Show resolvers:

func getAllReviews(
  request: Request,
  arguments: PaginationArguments
) throws -> EventLoopFuture<[Review]> {
  Review.query(on: request.db)
    .limit(arguments.limit)
    .offset(arguments.offset)
    .all()
}

struct CreateReviewArguments: Codable {
  let showID: UUID
  let title: String
  let text: String
}

func createReview(
  request: Request,
  arguments: CreateReviewArguments
) throws -> EventLoopFuture<Review> {
  let review = Review(
    showID: arguments.showID,
    title: arguments.title,
    text: arguments.text
  )
  return review.create(on: request.db).map { review }
}

struct UpdateReviewArguments: Codable {
  let id: UUID
  let title: String
  let text: String
}

func updateReview(
  request: Request,
  arguments: UpdateReviewArguments
) throws -> EventLoopFuture<Bool> {
  Review.find(arguments.id, on: request.db)
    .unwrap(or: Abort(.notFound))
    .flatMap { (review: Review) -> EventLoopFuture<()> in
      review.title = arguments.title
      review.text = arguments.text
      return review.update(on: request.db)
    }
    .transform(to: true)
}

struct DeleteReviewArguments: Codable {
  let id: UUID
}

func deleteReview(
  request: Request,
  arguments: DeleteReviewArguments
) -> EventLoopFuture<Bool> {
  Review.find(arguments.id, on: request.db)
    .unwrap(or: Abort(.notFound))
    .flatMap { $0.delete(on: request.db) }
    .transform(to: true)
}

Each function here runs a database statement that takes arguments passed in a corresponding Codable structure to perform the CRUD operations on Review‘s.

Now you need to update the schema in Schema.swift. In Schema.swift, add a new Type declaration just above the existing one for Type(Show.self):

Type(Review.self) {
  Field("id", at: \.id)
  Field("title", at: \.title)
  Field("text", at: \.text)
}

The existing Show schema also needs modifications to take advantage of the new extension. Add a new reviews field to the bottom of this Type:

Field("reviews", at: Show.getReviews) {
  Argument("limit", at: \.limit)
  Argument("offset", at: \.offset)
}
Note: It’s important to follow the specified order of Type blocks. The Graphiti library builds the schema in the same order you specify its types. Since the Show GraphQL type references the Review type through the reviews field, you have to put the Type(Review.self) block above the Type(Show.self) block in the schema definition.

This is not a restriction of GraphQL itself but a technical limitation of Graphiti.

Now update the Query block by adding a new reviews below shows:

Field("reviews", at: Resolver.getAllReviews) {
  Argument("limit", at: \.limit)
  Argument("offset", at: \.offset)
}

Note these last two fields are almost identical, but use two different functions to get their results. The first reviews field on the Show type fetches reviews for that specific show with the Show.getReview you’ve defined in the extension. The second field definition uses Resolver.getAllReviews, which fetches all reviews across the whole database.

Finally, add remaining mutations at the bottom of Mutation:

Field("createReview", at: Resolver.createReview) {
  Argument("showID", at: \.showID)
  Argument("title", at: \.title)
  Argument("text", at: \.text)
}

Field("updateReview", at: Resolver.updateReview) {
  Argument("id", at: \.id)
  Argument("title", at: \.title)
  Argument("text", at: \.text)
}

Field("deleteReview", at: Resolver.deleteReview) {
  Argument("id", at: \.id)
}

Build and run. Then refresh the browser tab with the GraphiQL client. As expected, new queries, fields and mutations appear in the documentation explorer.

Updated GraphQL documentation

Add a few shows to the database and note their identifiers. You can then pass them to the new createReview mutation to submit reviews.

When you have a few instances of each type in the database, you can compose deep queries that fetch fields across relations. Try this one, which fetches a maximum of ten shows and ten reviews from each show:

query {
  shows(limit: 10, offset: 0) {
    id
    title
    reviews(limit:10 offset: 0) {
      id
      title
      text
    }
  }
}

The GraphiQL client with results of the shows query with relationships traversed

Congratulations, you have a fully functional GraphQL API server that can fetch entities of different types and paginate across relationships between them!

Where to Go From Here?

To compare your results with the final project, click Download Materials at the bottom or the top of this page.

Great, you’ve finished this introduction to GraphQL using Server Side Swift. In this article you’ve learnt the following:

  • How GraphQL differs from other technologies such as pure REST implementations.
  • Define Fields, Queries and Mutations in GraphQL.
  • Learnt how to create, update and delete entities within your model.
  • Exploring the GraphQL API.
  • Use pagination to retrieve multiple pages of results

If you’d like to build an iOS app that uses this GraphQL API, check out the Apollo iOS library and GraphQL Using the Apollo Framework. If you’re working on a web frontend app, the Apollo Client library for JavaScript and TypeScript is a great option. For a deeper dive into the GraphQL ecosystem, I highly recommend the GraphQL Foundation website.

If you have any questions or feedback, please feel free to join the forum discussion below!