Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Server Side Swift and GraphQL: A Match Made In ...

Server Side Swift and GraphQL: A Match Made In Heaven Or Hell? - Deep Dish Swift, Chicago IL, May 2023

I like server side Swift. I also like GraphQL. What happens when you try to put them together? Am I getting chocolate in my peanut butter, or am I creating a monster?

This talk walks through some of the very basics of servers and how GraphQL works, all the way up to running live GraphQL queries on a Vapor server.

Repo: https://github.com/designatednerd/MoveItMoveIt/

Recording: ✨Coming soon! ✨

Ellen Shapiro

May 02, 2023
Tweet

More Decks by Ellen Shapiro

Other Decks in Technology

Transcript

  1. SERVER-SIDE SWIFT AND GRAPHQL: A MATCH MADE IN ! HEAVEN

    OR HELL ? DEEP DISH SWIFT | CHICAGO, IL | MAY 2023 ELLEN SHAPIRO | MASTODON.SOCIAL/@DESIGNATEDNERD | PIXITEAPPS.COM
  2. "Docker is a container virtualization platform that builds on tools

    like Virtual Box!" @DesignatedNerd@mastodon.social
  3. YOUR APP'S DATA THE INFORMATION YOU WANT TO STORE, MANIPULATE

    AND/OR DISPLAY @DesignatedNerd@mastodon.social
  4. THE SERVER THE PLACE WHERE THE CANONICAL VERSION OF YOUR

    DATA LIVES @DesignatedNerd@mastodon.social
  5. THE SERVER > Some way to store data > Some

    way to fetch data from storage @DesignatedNerd@mastodon.social
  6. THE SERVER > Some way to store data > Some

    way to fetch data from storage > Some way to receive requests for data @DesignatedNerd@mastodon.social
  7. THE SERVER > Some way to store data > Some

    way to fetch data from storage > Some way to receive requests for data > Some way to return responses for requested data @DesignatedNerd@mastodon.social
  8. THE SERVER > Some way to store data > Some

    way to fetch data from storage > Some way to receive requests for data > Some way to return responses for requested data > Some security mechanisms @DesignatedNerd@mastodon.social
  9. FLUENT A SWIFT ADAPTER FOR TALKING TO MULTIPLE DATA STORAGE

    TYPES @DesignatedNerd@mastodon.social
  10. THE SERVER > Some way to store data > Some

    way to fetch data from storage > Some way to receive requests for data > Some way to return responses for requested data > Some security mechanisms @DesignatedNerd@mastodon.social
  11. THE SERVER > Some way to store data (Fluent) >

    Some way to fetch data from storage (Fluent) > Some way to receive requests for data > Some way to return responses for requested data > Some security mechanisms @DesignatedNerd@mastodon.social
  12. THE SERVER > Some way to store data (Fluent) >

    Some way to fetch data from storage (Fluent) > Some way to receive requests for data (Vapor) > Some way to return responses for requested data (Vapor) > Some security mechanisms (mostly Vapor) @DesignatedNerd@mastodon.social
  13. A CLIENT A THING THAT INTERACTS WITH DATA FROM YOUR

    SERVER @DesignatedNerd@mastodon.social
  14. A CLIENT > Requests data from the server > Processes

    and displays data from the server @DesignatedNerd@mastodon.social
  15. A CLIENT > Requests data from the server > Processes

    and displays data from the server > Processes and displays user input @DesignatedNerd@mastodon.social
  16. A CLIENT > Requests data from the server > Processes

    and displays data from the server > Processes and displays user input > Sends updated data to the server @DesignatedNerd@mastodon.social
  17. EXAMPLES OF CLIENTS > Website > iOS App > Android

    App @DesignatedNerd@mastodon.social
  18. EXAMPLES OF CLIENTS > Website > iOS App > Android

    App > Another server not owned by you @DesignatedNerd@mastodon.social
  19. EXAMPLES OF CLIENTS > Website > iOS App > Android

    App > Another server not owned by you > An IoT Device @DesignatedNerd@mastodon.social
  20. THE PUBLIC API THE DEFINITION OF HOW THE CLIENT AND

    SERVER CAN TALK TO EACH OTHER @DesignatedNerd@mastodon.social
  21. api/user/1 { "id": "1", "name": "Bart Simpson" "age": "10" "family_member_ids":

    [ "2", "3", "4", "5" ] } @DesignatedNerd@mastodon.social
  22. api/user/1: { "id": "1", "name": "Bart Simpson" "age": "10" "family_members":

    [ { "id": "2", "name": "Lisa Simpson", "age": 8 }, { "id": 3", "name": "Homer Simpson", "age": 39 }, { "id": "4", "name": "Marge Simpson", "age": 39 }, { "id": "5", "name": "Maggie Simpson", "age": 1 } ] } @DesignatedNerd@mastodon.social
  23. GRAPHQL A TYPE OF PUBLIC API THAT ALLOWS CLIENTS TO

    REQUEST SPECIFIC FIELDS FROM THE SERVER @DesignatedNerd@mastodon.social
  24. WHAT CAN I ASK FOR? HOW CAN I ASK FOR

    IT? @DesignatedNerd@mastodon.social
  25. "Oh no, we don't have any place where all the

    HTTP endpoints are written down. Just grep the codebase of the monolith, that should get you what you need." @DesignatedNerd@mastodon.social
  26. "Oh no, we don't have any place where all the

    HTTP endpoints are written down. Just grep the codebase of the monolith, that should get you what you need." (An actual thing someone told me to do the first week of a new job)
  27. THE GRAPHQL SCHEMA A CONTRACT OF WHAT CAN BE REQUESTED,

    WHAT WILL BE RETURNED, AND HOW @DesignatedNerd@mastodon.social
  28. IN THE SCHEMA > Scalars > Custom Types > Fields/Properties

    > Relationships (inferred) @DesignatedNerd@mastodon.social
  29. is Optional Swift Type GraphQL Type No String String! Yes

    String? @DesignatedNerd@mastodon.social
  30. is Optional Swift Type GraphQL Type No String String! Yes

    String? String @DesignatedNerd@mastodon.social
  31. A GRAPHQL OPERATION A DEFINITION OF A REQUEST TO FETCH,

    CHANGE, OR LISTEN TO CHANGES FOR YOUR DATA @DesignatedNerd@mastodon.social
  32. OPERATIONS > Query: I'd like some info, please > Mutation:

    I'd like to make a change @DesignatedNerd@mastodon.social
  33. OPERATIONS > Query: I'd like some info, please > Mutation:

    I'd like to make a change > Subscription: Keep me up to date on changes @DesignatedNerd@mastodon.social
  34. OPERATIONS > Query: I'd like some info, please > Mutation:

    I'd like to make a change > Subscription: Keep me up to date on changes @DesignatedNerd@mastodon.social
  35. // These all get pulled automatically with GraphQL kit, but

    you need // to explicitly declare them as dependencies to use them directly. .package(url: "https://github.com/vapor/vapor.git", .upToNextMajor(from: "4.0.0")), .package(url: "https://github.com/vapor/fluent.git", .upToNextMajor(from: "4.0.0")), .package(url: "https://github.com/vapor/fluent-sqlite-driver.git", .upToNextMajor(from: "4.0.0")), .package(url: "https://github.com/GraphQLSwift/Graphiti", .upToNextMinor(from: "0.26.0")), @DesignatedNerd@mastodon.social
  36. .target(name: "App", dependencies: [ .product(name: "Fluent", package: "fluent"), .product(name: "FluentSQLiteDriver",

    package: "fluent-sqlite-driver"), .product(name: "Vapor", package: "vapor"), .product(name: "GraphQLKit", package: "graphql-kit"), .product(name: "GraphiQLVapor", package: "graphiql-vapor"), ]), @DesignatedNerd@mastodon.social
  37. final class User: Model, Content { static var schema =

    "users" @ID(key: .id) var id: UUID? @Field(key: "email") var email: String @Field(key: "name") var name: String @Field(key: "hashedPassword") var hashedPassword: String @Children(for: \.$user) var moves: [Move] // ... } @DesignatedNerd@mastodon.social
  38. final class User: Model, Content { static var schema =

    "users" @ID(key: .id) var id: UUID? @Field(key: "email") var email: String @Field(key: "name") var name: String @Field(key: "hashedPassword") var hashedPassword: String @Children(for: \.$user) var moves: [Move] // ... } @DesignatedNerd@mastodon.social
  39. final class User: Model, Content { static var schema =

    "users" @ID(key: .id) var id: UUID? @Field(key: "email") var email: String @Field(key: "name") var name: String @Field(key: "hashedPassword") var hashedPassword: String @Children(for: \.$user) var moves: [Move] // ... } @DesignatedNerd@mastodon.social
  40. final class User: Model, Content { static var schema =

    "users" @ID(key: .id) var id: UUID? @Field(key: "email") var email: String @Field(key: "name") var name: String @Field(key: "hashedPassword") var hashedPassword: String @Children(for: \.$user) var moves: [Move] // ... } @DesignatedNerd@mastodon.social
  41. final class User: Model, Content { static var schema =

    "users" @ID(key: .id) var id: UUID? @Field(key: "email") var email: String @Field(key: "name") var name: String @Field(key: "hashedPassword") var hashedPassword: String @Children(for: \.$user) var moves: [Move] // ... } @DesignatedNerd@mastodon.social
  42. final class User: Model, Content { static var schema =

    "users" @ID(key: .id) var id: UUID? @Field(key: "email") var email: String @Field(key: "name") var name: String @Field(key: "hashedPassword") var hashedPassword: String @Children(for: \.$user) var moves: [Move] // ... } @DesignatedNerd@mastodon.social
  43. final class User: Model, Content { static var schema =

    "users" enum DatabaseKeys: String { case email case name case hashedPassword } @ID(key: .id) var id: UUID? @Field(key: "email") var email: String @Field(key: "name") var name: String @Field(key: "hashedPassword") var hashedPassword: String @Children(for: \.$user) var moves: [Move] // ... } @DesignatedNerd@mastodon.social
  44. final class User: Model, Content { static var schema =

    "users" enum DatabaseKeys: String { case email case name case hashedPassword } @ID(key: .id) var id: UUID? @Field(key: DatabaseKeys.email.fieldKey) var email: String @Field(key: DatabaseKeys.name.fieldKey) var name: String @Field(key: DatabaseKeys.hashedPassword.fieldKey) var hashedPassword: String @Children(for: \.$user) var moves: [Move] // ... } @DesignatedNerd@mastodon.social
  45. // configures your application public func configure(_ app: Application) throws

    { app.databases.use(.sqlite(.memory), as: .sqlite) // Create the appropriate migrations app.migrations.add(User.Migration()) app.migrations.add(Move.Migration()) try app.autoMigrate().wait() // Register the schema and its resolver. // TODO } @DesignatedNerd@mastodon.social
  46. // configures your application public func configure(_ app: Application) throws

    { app.databases.use(.sqlite(.memory), as: .sqlite) // Create the appropriate migrations app.migrations.add(User.Migration()) app.migrations.add(Move.Migration()) try app.autoMigrate().wait() // Register the schema and its resolver. // TODO } @DesignatedNerd@mastodon.social
  47. // configures your application public func configure(_ app: Application) throws

    { app.databases.use(.sqlite(.memory), as: .sqlite) // Create the appropriate migrations app.migrations.add(User.Migration()) app.migrations.add(Move.Migration()) try app.autoMigrate().wait() // Register the schema and its resolver. // TODO } @DesignatedNerd@mastodon.social
  48. RESOLVER FUNCTION TO GET DATA AND GIVE IT TO GQL

    @DesignatedNerd@mastodon.social
  49. let movingSchema = try! Schema<GraphQLResolver, Request> { Type(User.self) { Field("id",

    at: \.id) Field("email", at: \.email) Field("name", at: \.name) } } @DesignatedNerd@mastodon.social
  50. let movingSchema = try! Schema<GraphQLResolver, Request> { Type(User.self) { Field("id",

    at: \.id) Field("email", at: \.email) Field("name", at: \.name) } } @DesignatedNerd@mastodon.social
  51. let movingSchema = try! Schema<GraphQLResolver, Request> { Type(User.self) { Field("id",

    at: \.id) Field("email", at: \.email) Field("name", at: \.name) } } @DesignatedNerd@mastodon.social
  52. // configure.swift // Register the schema and its resolver. app.register(graphQLSchema:

    movingSchema, withResolver: GraphQLResolver()) @DesignatedNerd@mastodon.social
  53. "YOU FORGOT TO DECLARE THIS TYPE IN THE SCHEMA, YA

    DING DONG" @DesignatedNerd@mastodon.social
  54. let movingSchema = try! Schema<GraphQLResolver, Request> { Scalar(UUID.self) Type(User.self) {

    Field("id", at: \.id) Field("email", at: \.email) Field("name", at: \.name) } } @DesignatedNerd@mastodon.social
  55. let movingSchema = try! Schema<GraphQLResolver, Request> { Scalar(UUID.self) Type(User.self) {

    Field("id", at: \.id) Field("email", at: \.email) Field("name", at: \.name) } } @DesignatedNerd@mastodon.social
  56. let movingSchema = try! Schema<GraphQLResolver, Request> { Scalar(UUID.self) Type(User.self) {

    Field("id", at: \.id, as: UUID.self) Field("email", at: \.email) Field("name", at: \.name) } } @DesignatedNerd@mastodon.social
  57. Mutation { Field("createUser", at: GraphQLResolver.createUser) { Argument("name", at: \.name) Argument("email",

    at: \.email) Argument("password", at: \.password) } } @DesignatedNerd@mastodon.social
  58. Mutation { Field("createUser", at: GraphQLResolver.createUser) { Argument("name", at: \.name) Argument("email",

    at: \.email) Argument("password", at: \.password) } } @DesignatedNerd@mastodon.social
  59. Mutation { Field("createUser", at: GraphQLResolver.createUser) { Argument("name", at: \.name) Argument("email",

    at: \.email) Argument("password", at: \.password) } } @DesignatedNerd@mastodon.social
  60. struct UserCreateArguments: Codable { let name: String let email: String

    let password: String } extension GraphQLResolver { func createUser(request: Request, arguments: UserCreateArguments) throws -> EventLoopFuture<User> { let user = try User(email: arguments.email, password: arguments.password, name: arguments.name) return user.create(on: request.db).map { user } } } @DesignatedNerd@mastodon.social
  61. struct UserCreateArguments: Codable { let name: String let email: String

    let password: String } extension GraphQLResolver { func createUser(request: Request, arguments: UserCreateArguments) throws -> EventLoopFuture<User> { let user = try User(email: arguments.email, password: arguments.password, name: arguments.name) return user.create(on: request.db).map { user } } } @DesignatedNerd@mastodon.social
  62. struct UserCreateArguments: Codable { let name: String let email: String

    let password: String } extension GraphQLResolver { func createUser(request: Request, arguments: UserCreateArguments) throws -> EventLoopFuture<User> { let user = try User(email: arguments.email, password: arguments.password, name: arguments.name) return user.create(on: request.db).map { user } } } @DesignatedNerd@mastodon.social
  63. Mutation { Field("createUser", at: GraphQLResolver.createUser) { Argument("name", at: \.name) Argument("email",

    at: \.email) Argument("password", at: \.password) } } @DesignatedNerd@mastodon.social
  64. Mutation { Field("createUser", at: GraphQLResolver.createUser) { Argument("name", at: \.name) Argument("email",

    at: \.email) Argument("password", at: \.password) } } @DesignatedNerd@mastodon.social
  65. // Something to help visualize and test your schema and

    operations .package(url: "https://github.com/alexsteinerde/graphiql-vapor.git", .upToNextMajor(from: "2.0.0")), @DesignatedNerd@mastodon.social
  66. import GraphiQLVapor public func configure(_ app: Application) throws { //...

    (stuff you already saw) // Enable GraphiQL web page for built-in goodness if !app.environment.isRelease { app.enableGraphiQL() } } @DesignatedNerd@mastodon.social
  67. let corsConfiguration = CORSMiddleware.Configuration( allowedOrigin: .custom("https://studio.apollographql.com"), allowedMethods: [.POST], allowedHeaders: [

    .accept, .contentType, .accessControlAllowOrigin, .origin ] ) let cors = CORSMiddleware(configuration: corsConfiguration) app.middleware.use(cors) @DesignatedNerd@mastodon.social
  68. class MoveResolver { private let userResolver = UserResolver() // MARK:

    - Pass-Through Methods func createUser(request: Request, arguments: UserCreateArguments) throws -> EventLoopFuture<User> { try userResolver.createUser(request: request, arguments: arguments) } //... } @DesignatedNerd@mastodon.social
  69. GraphQLResolver+Moves.swift class GraphQLResolver { func createMove(request: Request, arguments: CreateMoveArguments) ->

    EventLoopFuture<Move> { //... } } GraphQLResolver+Users.swift extension GraphQLResolver func createUser(request: Request, arguments: UserCreateArguments) throws -> EventLoopFuture<User> { //... } } @DesignatedNerd@mastodon.social
  70. THE VERDICTS: > ! If you know a lot about

    both Vapor/Fluent AND GQL @DesignatedNerd@mastodon.social
  71. THE VERDICTS > ! If you know a lot about

    both Vapor/Fluent AND GQL > " If you know a lot about Vapor/Fluent but not GQL @DesignatedNerd@mastodon.social
  72. THE VERDICTS > ! If you know a lot about

    both Vapor/Fluent AND GQL > " If you know a lot about Vapor/Fluent but not GQL > # If you know a lot about GQL but not Vapor/Fluent @DesignatedNerd@mastodon.social
  73. THE VERDICTS > ! If you know a lot about

    both Vapor/Fluent AND GQL > " If you know a lot about Vapor/Fluent but not GQL > # If you know a lot about GQL but not Vapor/Fluent > $ If you don't know a lot about either one @DesignatedNerd@mastodon.social
  74. OBLIGATORY SUMMARY SLIDE > ! If you know a lot

    about both Vapor/Fluent AND GQL > " If you know a lot about Vapor/Fluent but not GQL > # If you know a lot about GQL but not Vapor/Fluent > $ If you don't know a lot about either one @DesignatedNerd@mastodon.social
  75. LINKS! > Vapor/Fluent Docs: https://docs.vapor.codes/ > GraphQLKit: https://github.com/alexsteinerde/ graphql-kit/ >

    (slightly outdated) Tutorial on getting started: https://www.kodeco.com/21148796-graphql- tutorial-for-server-side-swift-with-vapor- getting-started @DesignatedNerd@mastodon.social