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?
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)
}
This is not a restriction of GraphQL itself but a technical limitation of Graphiti.
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!