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.
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
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
GraphQL Tutorial for Server-Side Swift with Vapor: Getting Started
30 mins
- Getting Started
- How GraphQL Differs From REST
- Declaring Model Types
- GraphQL Fields, Queries and Mutations
- Defining GraphQL Queries
- Exploring the GraphQL API
- Defining GraphQL Mutations
- Adding Pagination to the GraphQL Schema
- Adding a Related Model Type
- Handling Parent-Child Relationships in GraphQL
- Where to Go From Here?
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:
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:
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())
Here’s what your project structure looks like after adding the new files:
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:
- The new
getAllShows
serves a response to an instance ofRequest
, whichVapor
declares. -
db
lets you access the database, and the staticShow.query
withall()
on top of it fetches all shows. - The second
arguments
parameter of typeNoArguments
fromGraphiti
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)
}
}
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.
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:
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:
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.
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)
}
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.
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
}
}
}
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!
All videos. All books.
One low price.
A Kodeco subscription is the best way to learn and master mobile development — plans start at just $19.99/month! Learn iOS, Swift, Android, Kotlin, Flutter and Dart development and unlock our massive catalog of 50+ books and 4,000+ videos.
Learn more