#
Get started with Pioneer
This tutorial will help you get started with building a GraphQL API using Pioneer.
This tutorial assumes that you are familiar with the command line, Swift, and Swift Package Manager and have installed a recent Swift version.
#
1: New Swift project
Go to a directory where you want to create the project on.
Setup a skeleton of the executable using Swift package manager by running:
swift package init --type executable
The project directory should now contains a Package.swift
file.
#
2: Dependencies
For this tutorial, we will be using Vapor as the web framework and Graphiti to built our GraphQL schema.
#
Adding dependencies
Add these dependencies and Pioneer to the Package.swift
let package = Package(
dependencies: [
.package(url: "https://github.com/GraphQLSwift/Graphiti.git", from: "1.2.1"),
.package(url: "https://github.com/vapor/vapor.git", from: "4.67.1"),
.package(url: "https://github.com/d-exclaimation/pioneer", from: "1.0.0")
],
targets: [
.target(
name: "<project-name>",
dependencies: [
.product(name: "Pioneer", package: "pioneer"),
.product(name: "Graphiti", package: "Graphiti"),
.product(name: "Vapor", package: "vapor")
]
)
]
)
#
Using Swift 5.5 or higher
We will also restrict the platform of the project to macOS v12 or higher, to allow the use of Swift Concurrency.
let package = Package(
platforms: [
.macOS(.v12)
],
// ...
)
#
3: Define entities and context
Continuing from the setup, now we will be declaring entities for the GraphQL API.
#
Book entity
We'll create a simple Book
entity.
import struct Pioneer.ID
struct Book: Identifiable {
var id: ID
var title: String
}
#
Context
Context is a useful type that can be generated for each request and can be used for many purpose such as:
- Reading request-specific header value,
- Setting response headers and cookies, or
- Performing dependency injection to each resolver functions
For this tutorial, we will create simple context type to hold both the Vapor's Request
and Response
object.
import class Vapor.Request
import class Vapor.Response
struct Context {
var req: Request
var res: Response
}
#
4: Define data source
Pioneer doesn't dictate where the resolvers get their data from and thus, it can be use with any data source (any databases, any ORMs, etc.).
For simplicity, we will simple hardcode the value and use actor to store it.
actor Books {
private var books: [Book.ID: Book] = [:]
func create(book: Book) -> Book? {
guard case .none = books[book.id] else {
return nil
}
books[book.id] = book
return book
}
func all() -> [Book] {
return books.values.map { $0 }
}
enum Errors: Error {
case duplicate(id: Book.ID)
}
static let shared: Books = .init()
}
#
5: Define resolver
Resolvers tell GraphQL schema how to fetch the data associated with a particular type.
#
Resolver
In Graphiti, this is done with a seperate resolver struct.
struct Resolver {}
#
Query resolver
Let's now add a resolver to query all the books
import struct Graphiti.NoArguments
struct Resolver {
func books(ctx: Context, args: NoArguments) async -> [Book] {
await Books.shared.all()
}
}
#
Mutation resolver and arguments
For a mutation, arguments may be necessary to provide information to create a new instance of a type.
In Graphiti, this is done with a seperate argument struct which must be Decodable
.
import struct Graphiti.NoArguments
import struct Vapor.Abort
struct Resolver {
func books(ctx: Context, args: NoArguments) async -> [Book] {
await Books.shared.all()
}
struct NewArgs: Decodable {
var title: String
}
func newBook(ctx: Context, args: NewArgs) async throws -> Book {
let book = await Books.shared.create(
book: Book(id: .uuid(), title: args.title)
)
guard let book else {
throw Abort(.internalServerError)
}
return book
}
}
#
6: Define Schema
Every GraphQL server uses a schema to define the structure of data that clients can query.
In Graphiti, schema can be declared using Swift code which allow type safety.
import Graphiti
import struct Pioneer.ID
func schema() throws -> Schema<Resolver, Context> {
.init {
// Adding ID as usable scalar for Graphiti
Scalar(ID.self)
// The Book as a GraphQL type with all its properties as fields
Type(Book.self) {
Field("id", at: \.id)
Field("title", at: \.title)
}
Query {
// The root query field to fetch all books
Field("books", at: Resolver.books)
}
Mutation {
// The root mutation field to create a new book
Field("newBook", at: Resolver.book) {
// Argument for this field
Argument("title", at: \.title)
}
}
}
}
GraphQL SDL
This is the equivalent schema in GraphQL SDL for one built with Graphiti. This is not required to be created.
type Book {
id: ID!
title: String!
}
type Query {
books: [Book!]!
}
type Mutation {
newBook(title: String!): Book!
}
schema {
query: Query
mutation: Mutation
}
For further reading, the team at Apollo GraphQL has provided a guide on good schema design.
#
7: Pioneer instance
Now, it's time to integrate Pioneer into the existing Vapor application using the resolver and schema declared before.
#
Pioneer configuration
First, create an instance of Pioneer with the desired configuration.
import Pioneer
let server = try Pioneer(
schema: schema(),
resolver: Resolver(),
httpStrategy: .csrfPrevention,
introspection: true,
playground: .sandbox
)
#
Basic Vapor application
Next, let's setup a basic Vapor application.
import Pioneer
import Vapor
let app = try Application(.detect())
let server = try Pioneer(
schema: schema(),
resolver: Resolver(),
httpStrategy: .csrfPrevention,
introspection: true,
playground: .sandbox
)
defer {
app.shutdown()
}
try app.run()
Pioneer can also run as a standalone server without needing to setup a Vapor application.
import Pioneer
let server = try Pioneer(
schema: schema(),
resolver: Resolver(),
httpStrategy: .csrfPrevention,
introspection: true,
playground: .sandbox
)
try server.standaloneServer(
at: "graphql",
context: { req, res in
Context(req, res)
}
)
#
Pioneer as Vapor middleware
Finally, apply Pioneer to Vapor as a middleware.
import Pioneer
import Vapor
let app = try Application(.detect())
let server = try Pioneer(
schema: schema(),
resolver: Resolver(),
httpStrategy: .csrfPrevention,
introspection: true,
playground: .sandbox
)
defer {
app.shutdown()
}
app.middleware.use(
server.vaporMiddleware(
at: "graphql",
context: { req, res in
Context(req, res)
}
)
)
try app.run()
#
8: Adding subscriptions
Subscriptions is a feature of GraphQL which allow real-time stream of data. This is usually done through WebSocket using an additional protocol.
#
Enabling GraphQL over WebSocket
Pioneer is already built with these feature, and all that you have to do is enable it.
import Pioneer
import Vapor
let app = try Application(.detect())
let server = try Pioneer(
schema: schema(),
resolver: Resolver(),
httpStrategy: .csrfPrevention,
websocketProtocol: .graphqlWs,
introspection: true,
playground: .sandbox
)
defer {
app.shutdown()
}
app.middleware.use(
server.vaporMiddleware(
at: "graphql",
context: { req, res in
Context(req, res)
}
)
)
try app.run()
#
Subscription resolver
Now, let's add the subscription resolver. Pioneer can resolve subscription as long as the return value is either:
- AsyncEventStream, or
ConcurrentEventStream
In this tutorial, we will be using Pioneer's built in PubSub system and its in-memory implementation, AsyncPubSub.
import Pioneer
import class GraphQL.EventStream
import struct Graphiti.NoArguments
import struct Vapor.Abort
struct Resolver {
private let pubsub: PubSub = AsyncPubSub()
private let trigger = "*:book-added"
func books(ctx: Context, args: NoArguments) async -> [Book] {
await Books.shared.all()
}
struct NewArgs: Decodable {
var title: String
}
func newBook(ctx: Context, args: NewArgs) async throws -> Book {
let book = await Books.shared.create(
book: Book(id: .uuid(), title: args.title)
)
guard let book else {
throw Abort(.internalServerError)
}
return book
}
func bookAdded(ctx: Context, args: NoArguments) -> EventStream<Book> {
pubsub.asyncStream(Book.self, for: trigger).toEventStream()
}
}
#
Triggering a subscription
With PubSub, subscription value can be pushed manually from a mutation. All we have to do is to publish under the same trigger.
import Pioneer
import class GraphQL.EventStream
import struct Graphiti.NoArguments
import struct Vapor.Abort
struct Resolver {
private let pubsub: PubSub = AsyncPubSub()
private let trigger = "*:book-added"
func books(ctx: Context, args: NoArguments) async -> [Book] {
await Books.shared.all()
}
struct NewArgs: Decodable {
var title: String
}
func newBook(ctx: Context, args: NewArgs) async throws -> Book {
let book = await Books.shared.create(
book: Book(id: .uuid(), title: args.title)
)
guard let book else {
throw Abort(.internalServerError)
}
await pubsub.publish(for: trigger, payload: book)
return book
}
func bookAdded(ctx: Context, args: NoArguments) -> EventStream<Book> {
pubsub.asyncStream(Book.self, for: trigger).toEventStream()
}
}
#
Updating the schema
We can now add the subscription in the schema.
import Graphiti
import struct Pioneer.ID
func schema() throws -> Schema<Resolver, Context> {
.init {
// Adding ID as usable scalar for Graphiti
Scalar(ID.self)
// The Book as a GraphQL type with all its properties as fields
Type(Book.self) {
Field("id", at: \.id)
Field("title", at: \.title)
}
Query {
// The root query field to fetch all books
Field("books", at: Resolver.books)
}
Mutation {
// The root mutation field to create a new book
Field("newBook", at: Resolver.book) {
// Argument for this field
Argument("title", at: \.title)
}
}
Subscription {
SubsciptionField("bookAdded", as: Book.self, atSub: Resolver.bookAdded)
}
}
}
#
WebSocket context
Due to the nature of subscription which goes through WebSocket instead of HTTP, the context is built with different types of information i.e. there is no Response
object for WebSocket operation.
Pioneer's Vapor integration allow a different WebSocket context builder which gives a different set of arguments catered towards what will be available on a WebSocket operation.
Pioneer will try to use the same context builder if not explicit given a different one for WebSocket. It will try to maintain all relevant information and inject that values into the Request
object.
import Pioneer
import Vapor
let app = try Application(.detect())
let server = try Pioneer(
schema: schema(),
resolver: Resolver(),
httpStrategy: .csrfPrevention,
websocketProtocol: .graphqlWs,
introspection: true,
playground: .sandbox
)
defer {
app.shutdown()
}
app.middleware.use(
server.vaporMiddleware(
at: "graphql",
context: { req, res in
Context(req, res)
},
websocketContext: { req, payload, gql in
Context(req, .init())
}
)
)
try app.run()
#
9: Start the server
The server is now ready!
Run the Swift project using:
swift run
Now, just open http://localhost:8080/graphql to go the Apollo Sandbox and play with the queries, and mutations.
🎉 Congrats
Congrats, you have just built a GraphQL server with Swift and Pioneer!