Using GraphQL instead of REST offers greater flexibility and efficiency. It allows clients to request precisely the data they need through a single endpoint, avoiding issues like over-fetching or under-fetching. Its strongly-typed schema enhances the developer experience by providing built-in documentation and easy introspection. Additionally, GraphQL’s real-time capabilities, enabled through subscriptions, support features such as live updates. It also excels at aggregating data from multiple sources into a unified API, making it an excellent choice for complex systems. However, it can introduce added server-side complexity and may not be necessary for simple or static applications where REST is sufficient.

In this post, we will create a minimal, dockerized GraphQL server and implement an iOS client app that performs a request. At the end of the post, you will find a link to a GitHub repository containing the source code for further review.

Setup a graphQL Server

In this section, we will develop a minimal GraphQL dockerized server. The purpose of this post is not to dive deeply into GraphQL or Docker. However, I recommend spending some time exploring tutorials on these topics. At the end of the post, you will find links to the tutorials I followed.

The server code fetches data from hardcoded sources for simplicity. In a typical scenario, the data would be retrieved from a database or other data source:

import { ApolloServer, gql } from 'apollo-server';

// Sample data
const users = [
    { id: '1', name: 'Brandon Flowers', email: 'brandon.flowers@example.com' },
    { id: '2', name: 'Dave Keuning', email: 'dave.keuning@example.com' },
    { id: '3', name: 'Ronnie Vannucci Jr.', email: 'ronnie.vannuccijr@example.com' },
    { id: '4', name: 'Mark Stoermer', email: 'mark.stoermer@example.com' },
  ];

// Schema
const typeDefs = gql`
  type Query {
    getUser(id: ID!): User
  }

  type User {
    id: ID!
    name: String!
    email: String!
  }
`;

// Resolver
const resolvers = {
  Query: {
    getUser: (_, { id }) => {
      const user =  users.find(user => user.id === id);
      if (!user) {
        throw new Error(`User with ID ${id} not found`);
      }
      return user;
    },
  },
};

// Setup server
const server = new ApolloServer({ typeDefs, resolvers });

// Start up server
server.listen().then(({ url }) => {
  console.log(`🚀 Servidor listo en ${url}`);
});
The server is containerized using Docker, eliminating the need to install npm on your local machine. It will be deployed within a Linux-based image preconfigured with Node.js:
# Usamos una imagen oficial de Node.js
FROM node:18

# Establecemos el directorio de trabajo
WORKDIR /usr/src/app

# Copiamos los archivos del proyecto a la imagen
COPY . .

# Instalamos las dependencias del proyecto
RUN npm install

# Exponemos el puerto 4000
EXPOSE 4000

# Ejecutamos el servidor
CMD ["node", "server.js"]

This Dockerfile packages a Node.js application into a container. When the container is run, it performs the following actions:

  1. Sets up the application directory.
  2. Installs the required dependencies.
  3. Starts the Node.js server located in server.js, making the application accessible on port 4000.

To build the Docker image, use the following command:

docker build -t graphql-server .
Once the image is built, simply run the container image
docker run -p 4000:4000 graphql-server
Type ‘http://localhost:4000/’ URL on your favourite browser:
The GraphQL server is now online. To start querying the server, simply click ‘Query your server,’ and the Sandbox will open for you to begin querying. The sample query that we will execute is as follows:
query  {
  getUser(id: "4") {
    id
    name
    email
  }
}
Up to this point, the server is ready to handle requests. In the next section, we will develop an iOS sample app client.

Sample iOS graphQL client app

For the sample iOS GraphQL client app, we will follow the MVVM architecture. The app will use Swift 6 and have Strict Concurrency Checking enabled. The app’s usage is as follows:

The user enters an ID (from 1 to 4), and the app prompts for the user’s name. The server then responds with the name associated with that ID. I will skip the view and view model components, as there is nothing new to discuss there. However, if you’re interested, you can find a link to the GitHub repository.

The key aspect of the implementation lies in the GraphQLManager, which is responsible for fetching GraphQL data. Instead of using a GraphQL SPM component like Apollo-iOS, I chose to implement the data fetching using URLSession. This decision was made to avoid introducing a third-party dependency. At this level, the code remains simple, and I will not expand further on this in the post.

Regarding Swift 6 compliance, the code is executed within a @GlobalActor to avoid overloading the @MainActor.

import SwiftUI
import Foundation

@globalActor
actor GlobalManager {
    static var shared = GlobalManager()
}

@GlobalManager
protocol GraphQLManagerProtocol {
    func fetchData(userId: String) async -> (Result<User, Error>)
}

@GlobalManager
class GraphQLManager: ObservableObject {

    @MainActor
    static let shared = GraphQLManager()

}

extension GraphQLManager: GraphQLManagerProtocol {

    func fetchData(userId: String) async -> (Result<User, Error>) {
        
        let url = URL(string: "http://localhost:4000/")!
        let query = """
        query  {
          getUser(id: "\(userId)") {
            id
            name
          }
        }
        """
        
        let body: [String: Any] = [
            "query": query
        ]
        guard let jsonData = try? JSONSerialization.data(withJSONObject: body) else {
            return .failure(NSError(domain: "Invalid JSON", code: 400, userInfo: nil))
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpBody = jsonData
        
        do {
            let (data, response) = try await URLSession.shared.data(for: request)
            guard let httpResponse = response as? HTTPURLResponse,
                (200...299).contains(httpResponse.statusCode) else {
                return .failure(ErrorService.invalidHTTPResponse)
            }
            do {
                let graphQLResponse = try JSONDecoder().decode(GraphQLResponse<GraphQLQuery>.self, from: data)
                return .success(graphQLResponse.data.user)
            } catch {
                return .failure(ErrorService.failedOnParsingJSON)
            }
        } catch {
            return .failure(ErrorService.errorResponse(error))
        }
    }
  
}

Conclusions

GraphQL is another alternative for implementing client-server requests. It does not differ significantly from the REST approach. You can find source code used for writing this post in following repository.

References

Copyright © 2024-2025 JaviOS. All rights reserved