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
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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)
}

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

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!

Max Desiatov

Contributors

Max Desiatov

Author

Kenny Dubroff

Tech Editor

Julia Zinchenko

Illustrator

Darren Ferguson

Final Pass Editor

Tim Condon

Team Lead

Over 300 content creators. Join our team.