Etiqueta: Client-Server

  • From Zero to SOAP

    From Zero to SOAP

    SOAP (Simple Object Access Protocol) is often chosen over REST or GraphQL for scenarios requiring high security, reliability, and formal contracts, particularly in enterprise environments. Its built-in support for WS-Security, strong typing, and detailed error handling makes it ideal for industries like finance or healthcare that demand strict compliance and complex, stateful transactions. SOAP’s WSDL provides a clear, formal contract between client and server, ensuring interoperability across different platforms and legacy systems. However, REST and GraphQL are generally preferred for modern web applications due to their simplicity, flexibility, and lower overhead, making them more suitable for mobile or web-based services where performance and ease of use are prioritized. The choice ultimately depends on the specific requirements of the project, with SOAP excelling in structured, secure, and complex use cases.

    In previous post we have explores API REST, GraphQL and Websocket network aplicatuib interfaces, now is turn of SOAP. In this post we are going to develop a simple dockerized NodeJS SOAP server that offers an add callculation service.

    Addition SOAP Server

    First step is to build a SOAP server that will implement arithmethic add operation. The server technology used by its simpicity is a Dockerized NodeJS server. Let’s create a nodeJS server from scratch

    npm init -y

    Next install server dependencies for soap and express

    npm install soap express

    This is SOAP server itself:

    const express = require('express');
    const soap = require('soap');
    const http = require('http');
    
    const app = express();
    
    const service = {
      MyService: {
        MyPort: {
          AddNumbers: function (args) {
            const aValue = parseInt(args.a , 10);
            const bValue = parseInt(args.b , 10);
            console.log(' args.a + args.b',  aValue + bValue);
            return { result: aValue + bValue };
          },
        },
      },
    };
    
    const xml = `
    <definitions name="MyService"
      targetNamespace="http://example.com/soap"
      xmlns="http://schemas.xmlsoap.org/wsdl/"
      xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
      xmlns:tns="http://example.com/soap"
      xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    
      <message name="AddNumbersRequest">
        <part name="a" type="xsd:int"/>
        <part name="b" type="xsd:int"/>
      </message>
      
      <message name="AddNumbersResponse">
        <part name="result" type="xsd:int"/>
      </message>
    
      <portType name="MyPort">
        <operation name="AddNumbers">
          <input message="tns:AddNumbersRequest"/>
          <output message="tns:AddNumbersResponse"/>
        </operation>
      </portType>
    
      <binding name="MyBinding" type="tns:MyPort">
        <soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/>
        <operation name="AddNumbers">
          <soap:operation soapAction="AddNumbers"/>
          <input>
            <soap:body use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
          </input>
          <output>
            <soap:body use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
          </output>
        </operation>
      </binding>
    
      <service name="MyService">
        <port name="MyPort" binding="tns:MyBinding">
          <soap:address location="http://localhost:8000/wsdl"/>
        </port>
      </service>
    </definitions>
    `;
    
    const server = http.createServer(app);
    server.listen(8000, () => {
      console.log('SOAP server running on http://localhost:8000/wsdl');
    });
    
    soap.listen(server, '/wsdl', service, xml);
    

    This Node.js script creates a SOAP web service using the express and soap libraries. The service, named MyService, exposes a single operation called AddNumbers, which takes two integer arguments (a and b), adds them together, and returns the result. The WSDL (Web Services Description Language) XML definition specifies how clients can interact with this service, including the request and response message structures, binding details, and the service endpoint (http://localhost:8000/wsdl). The server listens on port 8000, and when a SOAP request is received, it executes the AddNumbers operation and logs the sum to the console.

    The soap.listen() function attaches the SOAP service to the HTTP server, making it accessible to clients that send SOAP requests. The AddNumbers function extracts and parses the input arguments from the request, computes their sum, and returns the result in a SOAP response. This setup enables interoperability with SOAP-based clients that conform to the specified WSDL schema.

    Lets dockerize server:

    # Official image for Node.js
    FROM node:18
    
    # Fix working directory
    WORKDIR /app
    
    # Copy necessary files
    COPY package.json package-lock.json ./
    RUN npm install
    
    # Copy rest of files
    COPY . .
    
    # Expose server port
    EXPOSE 8000
    
    # Command for executing server
    CMD ["node", "server.js"]

    This Dockerfile sets up a containerized environment for running a Node.js application. It starts with the official Node.js 18 image as the base. The working directory inside the container is set to /app. It then copies the package.json and package-lock.json files into the container and runs npm install to install dependencies. After that, it copies the rest of the application files into the container. The file exposes port 8000, which is likely used by the Node.js server. Finally, it specifies the command to run the server using node server.js when the container starts.

    Build the image from command line:

    docker build -t soap-server .

    … and execute the image:

    docker run -p 8000:8000 soap-server

    Server ready, now is turn of Swift iOS App client.

    iOS SOAP Client

    iOS app client UI interface is just two textfield for fill in the input operator for the addition, a button for executing the operation and finally a text labed that presents the results.

    import SwiftUI
    
    struct ContentView: View {
        @State private var result: String = ""
        @State private var aStr: String = ""
        @State private var bStr: String = ""
    
        var body: some View {
            VStack {
                Group {
                    TextField("a", text: $aStr)
                    TextField("b", text: $bStr)
                }
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
    
                Button("Do remote addition") {
                    guard let aInt = Int(aStr), let bInt = Int(bStr) else {
                        return
                    }
                    callSoapService(a: aInt, b: bInt) { response in
                        DispatchQueue.main.async {
                            self.result = response
                        }
                    }
                }            .padding()
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(15)
                
                Text("a+b= \(result)")
                    .font(.title)
                    .padding()
            }
        }

    When button is tapped then callSoaService  function is being called:

    import SwiftUI
    
    struct ContentView: View {
        @State private var result: String = ""
        @State private var aStr: String = ""
        @State private var bStr: String = ""
    
        var body: some View {
            VStack {
                Group {
                    TextField("a", text: $aStr)
                    TextField("b", text: $bStr)
                }
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
    
                Button("Do remote addition") {
                    guard let aInt = Int(aStr), let bInt = Int(bStr) else {
                        return
                    }
                    callSoapService(a: aInt, b: bInt) { response in
                        DispatchQueue.main.async {
                            self.result = response
                        }
                    }
                }            .padding()
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(15)
                
                Text("a+b= \(result)")
                    .font(.title)
                    .padding()
            }
        }

    This Swift function callSoapService(a:b:completion:) sends a SOAP request to a web service at http://localhost:8000/wsdl, passing two integers (a and b) to the AddNumbers method. It constructs an XML SOAP message, sends it via an HTTP POST request, and processes the response asynchronously using URLSession. The response is parsed to extract the result from the <tns:result> tag using a regular expression in the extractResult(from:) function, returning it via a completion handler.

    If the web service is running correctly, it should return the sum of a and b. However, the function may fail if the server is unavailable, the response format changes, or if the SOAP service requires additional headers. Also, the regular expression parsing method may not be robust against namespace variations.

    Build and run iOS app:

     

    Conclusions

    SOAP does not provide the flexibility that provides a regular API REST or GraphQL, but for those scenarios wher do we need a very strict API contracts is quite suitable technology.

    You can find source code used for writing this post in following repository

  • Seamless Apple Sign-In for iOS Apps with a Node.js Backend

    Seamless Apple Sign-In for iOS Apps with a Node.js Backend

    Implementing Sign in with Apple in a client-server setup is valuable because it addresses a real-world need that many developers face, especially as Apple requires apps offering third-party login to support it. While Apple’s documentation focuses mainly on the iOS side, there’s often a gap in clear explanations for securely validating Apple ID tokens on the backend — a critical step to prevent security vulnerabilities.

    Since Node.js is a widely used backend for mobile apps, providing a practical, end-to-end guide would help a large audience, fill a common knowledge gap, and position you as an expert who understands both mobile and server-side development, making the post highly useful, shareable, and relevant.

    Apple Sign in

    Apple Sign In is a secure authentication service introduced by Apple in 2019, allowing users to log in to apps and websites using their Apple ID. It emphasizes privacy by minimizing data sharing with third parties, offering features like hiding the user’s real email address through a unique, auto-generated proxy email. Available on iOS, macOS, and web platforms, it provides a fast and convenient alternative to traditional social logins like Google or Facebook.

    Advantages of Apple Sign In
    One of the biggest advantages is enhanced privacy—Apple does not track user activity across apps, and the «Hide My Email» feature protects users from spam and data leaks. It also simplifies the login process with Face ID, Touch ID, or device passcodes, reducing password fatigue. Additionally, Apple Sign In is mandatory for apps that offer third-party logins on iOS, ensuring wider adoption and consistent security standards.

    Inconveniences of Apple Sign In
    A major drawback is its limited availability, as it only works on Apple devices, excluding Android and Windows users. Some developers also criticize Apple for forcing its use on iOS apps while restricting competitor login options. Additionally, if a user loses access to their Apple ID, account recovery can be difficult, potentially locking them out of linked services. Despite these issues, Apple Sign In remains a strong choice for privacy-focused users.

    Dockerized Node.JS server side

    Start by setting up a blank Node.js server using Express.js to handle HTTP requests.

    npm init -y

    Server.js code is following:

    const express = require('express');
    const jwt = require('jsonwebtoken');
    const jwksClient = require('jwks-rsa');
    require('dotenv').config();
    
    const app = express();
    const PORT = process.env.PORT || 3000;
    
    // Middleware for parsing JSON
    app.use(express.json());
    
    // Client for look up public keys at Apple
    const client = jwksClient({
        jwksUri: 'https://appleid.apple.com/auth/keys'
    });
    
    // Function for getting public key
    function getAppleKey(header, callback) {
        client.getSigningKey(header.kid, function (err, key) {
            if (err) {
                callback(err);
            } else {
                const signingKey = key.getPublicKey();
                callback(null, signingKey);
            }
        });
    }
    
    // Route for authenticate
    app.post('/auth/apple', (req, res) => {
        const { identityToken } = req.body;
    
        if (!identityToken) {
            return res.status(400).json({ error: 'identityToken missing' });
        }
    
        jwt.verify(identityToken, getAppleKey, {
            algorithms: ['RS256']
        }, (err, decoded) => {
            if (err) {
                console.error('Error verifying token:', err);
                return res.status(401).json({ error: 'Invalid token' });
            }
    
            // decoded contains user data
            console.log('Token verified:', decoded);
    
            res.json({
                success: true,
                user: {
                    id: decoded.sub,
                    email: decoded.email,
                    email_verified: decoded.email_verified
                }
            });
        });
    });
    
    app.listen(PORT, () => {
        console.log(`Server listening on port ${PORT}`);
    });
    

    server.js sets up an Express server that listens for authentication requests using Apple’s Sign-In service. It imports necessary modules like express for routing, jsonwebtoken for verifying JSON Web Tokens (JWTs), and jwks-rsa for retrieving Apple’s public keys used to validate tokens. The server is configured to parse incoming JSON payloads and uses environment variables (loaded via dotenv) to optionally define a custom port.

    The core logic resides in the /auth/apple POST route. When a client sends a request to this endpoint with an identityToken in the body (typically issued by Apple after a successful login), the server first checks if the token is present. It then verifies the token using jsonwebtoken.verify(), passing a custom key retrieval function (getAppleKey). This function uses the jwksClient to fetch the appropriate public key from Apple’s JWKS (JSON Web Key Set) endpoint based on the kid (Key ID) found in the token header.

    If the token is valid, the decoded payload—which includes user-specific data like sub (user ID), email, and email_verified—is extracted and returned in the response as JSON. If token verification fails, an error response with HTTP 401 status is sent. This setup allows backend applications to securely validate Apple identity tokens without hardcoding public keys, keeping the authentication mechanism both dynamic and secure.

    Server is dockerized:

    FROM node:20
    WORKDIR /usr/src/app
    COPY package*.json ./
    RUN npm install
    COPY . .
    EXPOSE 3000
    CMD ["npm", "start"]

    This Dockerfile sets up a Node.js environment using the node:20 base image, creates a working directory at /usr/src/app, copies package.json and package-lock.json (if present) into it, installs dependencies with npm install, copies the rest of the application files, exposes port 3000 for the container, and finally runs the npm start command to launch the application.

    For building the app just type:

    docker build -t apple-signin-server .

    Finally execute the container:

    docker run -p 3000:3000 apple-signin-server

    Server ready for receiving requests…

    Client iOS Apple Sign in app

    After creating a simple iOS app project, go to the target settings and add the ‘Sign in with Apple’ capability. Then, start by creating a blank Node.js server.

    The next step is the client code itself:

    import SwiftUI
    import AuthenticationServices
    
    struct ContentView: View {
        @State private var userID: String?
        @State private var userEmail: String?
        @State private var userName: String?
        
        var body: some View {
            VStack(spacing: 20) {
                if let userID = userID {
                    Text("Welcome 🎉")
                        .font(.title)
                    Text("User ID: \(userID)")
                    if let name = userName {
                        Text("Name: \(name)")
                    }
                    if let email = userEmail {
                        Text("Email: \(email)")
                    }
                } else {
                    SignInWithAppleButton(
                        .signIn,
                        onRequest: { request in
                            request.requestedScopes = [.fullName, .email]
                        },
                        onCompletion: { result in
                            switch result {
                            case .success(let authorization):
                                handleAuthorization(authorization)
                            case .failure(let error):
                                print("Authentication error: \(error.localizedDescription)")
                            }
                        }
                    )
                    .signInWithAppleButtonStyle(.black)
                    .frame(width: 280, height: 50)
                    .cornerRadius(8)
                    .padding()
                }
            }
            .padding()
        }
        
        private func handleAuthorization(_ authorization: ASAuthorization) {
            if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
                userID = appleIDCredential.user
                userEmail = appleIDCredential.email
                if let fullName = appleIDCredential.fullName {
                    userName = [fullName.givenName, fullName.familyName]
                        .compactMap { $0 }
                        .joined(separator: " ")
                }
                
                if let identityToken = appleIDCredential.identityToken,
                   let tokenString = String(data: identityToken, encoding: .utf8) {
                    authenticateWithServer(identityToken: tokenString)
                }
            }
        }
        
        private func authenticateWithServer(identityToken: String) {
            guard let url = URL(string: "http://localhost:3000/auth/apple") else { return }
            
            var request = URLRequest(url: url)
            request.httpMethod = "POST"
            request.addValue("application/json", forHTTPHeaderField: "Content-Type")
            
            let body = ["identityToken": identityToken]
            
            request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: [])
            
            URLSession.shared.dataTask(with: request) { data, response, error in
                if let data = data,
                   let json = try? JSONSerialization.jsonObject(with: data) {
                    print("Server response:", json)
                } else {
                    print("Error communicating with server:", error?.localizedDescription ?? "Unknown error")
                }
            }.resume()
        }
    }
    
    
    #Preview {
        ContentView()
    }
    

    It defines a user interface for an iOS app that integrates Sign in with Apple. The core logic is built into the ContentView struct, which maintains state variables to store the signed-in user’s ID, name, and email. When the view is rendered, it checks whether the user is already signed in (i.e., if userID is not nil). If the user is authenticated, it displays a welcome message along with the retrieved user details. If not, it shows a «Sign in with Apple» button that initiates the authentication process when tapped.

    When the «Sign in with Apple» button is pressed, it triggers a request for the user’s full name and email. The result of this action is handled in the onCompletion closure. If the sign-in is successful, the handleAuthorization method is called. This function extracts the user’s credentials from the ASAuthorizationAppleIDCredential object, including their user ID, email, and full name (if provided). It also extracts the identity token (a JSON Web Token), which is used to authenticate the user on the app’s backend server.

    The authenticateWithServer function handles the server-side communication. It sends a POST request to http://localhost:3000/auth/apple, passing the identityToken in the JSON body. This token can be verified on the backend to ensure the identity is legitimate and secure. The response from the server (or any error encountered) is printed to the console. This architecture supports secure, privacy-preserving user login using Apple’s authentication services, commonly used in modern iOS apps.

    Apple Sign in integration

    Deploy iOS app with Apple Sign-In in a simulator (not on a real device).

    review

    Simply sign in using your personal iCloud credentials. Once Apple Sign-In is successful on the client side, it sends a request and provides the identityToken.

    Even if you uninstall the app from the device, the identityToken remains unchanged. Therefore, it can reliably be used as a user identifier.

    Conclusions

    From a programming perspective, implementing Apple Sign-In in your apps is straightforward and enhances privacy, as users can choose whether to share their email.

    You can find source code used for writing this post in following repository

    References

  • Enhancing iOS Apps with Redis

    Enhancing iOS Apps with Redis

    Connecting an iOS app to a Redis server can be highly beneficial, as it bridges the gap between mobile development and scalable backend solutions. Redis, known for its speed and efficiency, enhances performance by enabling fast caching, real-time data synchronization, and low-latency operations—critical for features like offline data storage, live notifications, and session management.

    In this post, we will build a Dockerized Node.js backend connected to a Dockerized Redis server. The client will be a simple iOS app that fetches a price list and updates the prices as well.

    Redis server

    The server is a sample Node.js application that will act as a backend facade. Let’s create a blank Node.js project:

    npm init -y

    To implement the server, we will need dotenv for loading environment variables from a .env file, express for setting up REST API endpoints, and ioredis for connecting to the Redis server.

    npm install dotenv express ioredis

    Node.Js server is following:

    require('dotenv').config(); // Fetch environment variables
    
    const express = require('express');
    const Redis = require('ioredis');
    
    const app = express();
    
    // Fetch environment variables
    const PORT = process.env.PORT || 3000;
    const REDIS_HOST = process.env.REDIS_HOST || 'localhost';
    const REDIS_PORT = process.env.REDIS_PORT || 6379;
    
    // Connect with Redis by using environnment variables
    const redis = new Redis({ host: REDIS_HOST, port: REDIS_PORT });
    
    app.use(express.json());
    
    // 📌 Init some prices
    async function initializeSamplePrices() {
        const initialPrices = {
            "iphone": "999",
            "macbook": "1999",
            "ipad": "799",
            "airpods": "199"
        };
    
        for (const [product, price] of Object.entries(initialPrices)) {
            await redis.set(`price:${product}`, price);
        }
    
        console.log("✅ Prices initialized in Redis.");
    }
    
    // Initialize sample prices
    initializeSamplePrices();
    
    // 📌 Endpoint for getting a product price
    app.get('/price/:product', async (req, res) => {
        const price = await redis.get(`price:${req.params.product}`);
        
        if (price) {
            res.json({ product: req.params.product, price: price });
        } else {
            res.status(404).json({ error: "Product not found" });
        }
    });
    
    app.get('/prices', async (req, res) => {
        try {
            const keys = await redis.keys("price:*"); 
            
            // Fetch all prices
            const prices = await Promise.all(keys.map(async (key) => {
                const product = key.replace("price:", "");
                const price = await redis.get(key); 
                return { product: product, price: price };
            }));
    
            res.json(prices);
        } catch (error) {
            console.error("Error on getting prices:", error);
            res.status(500).json({ error: "Error on getting prices" });
        }
    });
    
    // 📌 Endpoint for adding or updating a product price
    app.post('/price', async (req, res) => {
        const { product, price } = req.body;
        if (!product || !price) {
            return res.status(400).json({ error: "Misssing data" });
        }
        
        await redis.set(`price:${product}`, price);
        res.json({ mensaje: "Price stored", product, price });
    });
    
    // 📌 Server listening on specied port
    app.listen(PORT, () => console.log(`🚀 Server running on http://localhost:${PORT}`));
    

    This Node.js application sets up an Express server that interacts with a Redis database to store and retrieve product prices. It begins by loading environment variables using dotenv and establishes a connection to Redis with configurable host and port settings. When the server starts, it initializes Redis with some sample product prices (iPhone, MacBook, iPad, and AirPods). The Express app is configured to parse JSON requests, enabling it to handle API interactions effectively.

    The application provides multiple API endpoints: one for retrieving the price of a specific product (GET /price/:product), another for fetching all stored product prices (GET /prices), and a third for adding or updating a product price (POST /price). When a client requests a price, the server queries Redis, returning the value if found or a 404 error otherwise. The /prices endpoint retrieves all stored product prices by listing Redis keys and mapping them to their values. The /price POST endpoint allows clients to add or update prices by sending a JSON payload. Finally, the server listens on the configured port and logs a message indicating it is running.

    Sensitive intormateion is stored  in .env file:

    # Redis Configuration
    REDIS_HOST=redis
    REDIS_PORT=6379
    
    # Server Configuration
    PORT=3000

    Important: Please add .env to .gitignore to avoid compromising critical information in production environments. The next step is to define the Dockerfile for the Node.js server to ensure it is properly dockerized.

    FROM node:18
    WORKDIR /app
    COPY package*.json ./
    RUN npm install
    COPY . .
    EXPOSE 3000
    CMD ["node", "server.js"]

    The Dockerfile sets up a containerized Node.js application. It begins with the official Node.js 18 image, sets the working directory to /app, and copies the package.json and package-lock.json files into the container. Then, it installs the dependencies using npm install. Afterward, it copies the rest of the application files into the container. The container exposes port 3000 and specifies node server.js as the command to run when the container starts, which typically launches the application server.

    To strictly copy only the necessary files into the container, create the following .dockerignore file:

    node_modules
    npm-debug.log
    .env
    .git
    

    The backend solution consists of two containerized servers. On one side, we have a Node.js backend facade that interacts with the iOS app. On the other side, the backend relies on cache storage in a Redis server.

    services:
      redis:
        image: redis:latest
        container_name: redis-server
        ports:
          - "6379:6379"
        restart: always
    
      backend:
        build: .
        container_name: node-backend
        ports:
          - "${PORT}:${PORT}"
        depends_on:
          - redis
        env_file:
          - .env

    This code defines a docker-compose.yml configuration file for setting up two services: a Redis server and a backend Node.js application. The Redis service uses the latest Redis image, exposing port 6379 and ensuring the container always restarts. The backend service is built from the current directory (.) using a Dockerfile, exposes a dynamic port based on the environment variable ${PORT}, and depends on the Redis service. Additionally, the backend container loads environment variables from a .env file. The depends_on directive ensures Redis starts before the backend service.

    Build image and deploy containers:

    docker-compose up --build -d

    Make sure both containers are running by using docker ps. Finally, type the following URL in any browser to fetch the prices: http://localhost:3000/prices.

    Backend side is ready!

    iOS Sample App

    The iOS sample app fetches product prices and allows users to update any price.

    Core app functionallity is following:

    import Foundation
    
    struct Product: Codable, Identifiable, Sendable {
        var id: String { product }
        let product: String
        var price: String
    }
    
    @MainActor
    class PriceService: ObservableObject {
        @Published var products: [Product] = []
    
        func fetchPrices() async {
            guard let url = URL(string: "http://localhost:3000/prices") else { return }
    
            do {
                let (data, _) = try await URLSession.shared.data(from: url)
                let decoder = JSONDecoder()
                if let productos = try? decoder.decode([Product].self, from: data) {
                    self.products = productos
                }
            } catch {
                print("Error fetching prices:", error)
            }
        }
    
        func updatePrice(product: String, price: String) async {
            guard let url = URL(string: "http://localhost:3000/price") else { return }
            var request = URLRequest(url: url)
            request.httpMethod = "POST"
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    
            let body: [String: String] = ["product": product, "price": price]
            request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: [])
    
            do {
                let (_, _) = try await URLSession.shared.data(for: request)
                await self.fetchPrices()
            } catch {
                print("Error updating price: \(error.localizedDescription)")
            }
        }
    }

    This Swift code defines a model and a service for fetching and updating product prices. The Product struct represents a product with an ID, name, and price, conforming to the Codable, Identifiable, and Sendable protocols. The PriceService class is an observable object that manages a list of products and provides methods for interacting with a remote server.

    The fetchPrices() function asynchronously fetches product prices from a specified URL (http://localhost:3000/prices) using URLSession, decodes the returned JSON into an array of Product objects, and updates the products array. The updatePrice(product:price:) function sends a POST request to update the price of a specific product by sending a JSON payload to http://localhost:3000/price. After a successful update, it calls fetchPrices() again to refresh the list of products. Both functions handle errors by printing error messages if the network request fails.

    In this review, we update the price using the app and then return to the browser to verify that the price update has been reflected in the Redis server.

    Conclusions

    In this project, we demonstrated how easy it is to set up a sample backend server using Redis and interact with it through an iOS app as the client.

    You can find source code used for writing this post in following repository

    References

  • WebSockets Made Easy: Create a Simple Chat App in iOS

    WebSockets Made Easy: Create a Simple Chat App in iOS

    In this post, I will highlight how WebSockets enable real-time communication with minimal complexity. By leveraging WebSockets, developers can implement instant message delivery without relying on complex polling or delayed responses. This is crucial for providing a smooth user experience in chat applications. With iOS’s native support for WebSockets—such as URLSessionWebSocketTask—this post will demonstrate a simple, modern, and efficient solution for real-time messaging, while teaching developers essential skills like asynchronous communication and network management.

    In this tutorial, we will create a server using a Dockerized Node.js environment and two client applications: a simple HTML-JavaScript client and an iOS app WebSocket client.

    Websocket chat server

    To avoid confusion, let’s create a server folder to store all the necessary files. The first step is to create a new, blank Node.js project

    npm init -y
    Next setup library dependencies required.
    npm install ws express cors
    The libraries ws, express, and cors are installed on the server-side to provide essential functionalities for a modern web application. The ‘ws’ library enables WebSocket implementation in Node.js, allowing real-time bidirectional communication between clients and the server, which is crucial for chat applications. Express is a web application framework for Node.js that simplifies the creation of HTTP servers and route handling, making it easier to set up and manage the web application. Lastly, the ‘cors’ library is used to enable Cross-Origin Resource Sharing (CORS), a security mechanism that controls access to resources from different domains, ensuring that the server can safely interact with clients from various origins. Together, these libraries create a robust server capable of handling WebSocket connections, efficient HTTP routing, and secure cross-origin resource sharing.
    ‘server.js’ will contain our server code:
    const WebSocket = require('ws');
    const express = require('express');
    const cors = require('cors');
    
    const app = express();
    app.use(cors());
    
    const server = app.listen(8080, () => {
      console.log('Servidor HTTP escuchando en el puerto 8080');
    });
    
    const wss = new WebSocket.Server({ server });
    
    wss.on('connection', (ws) => {
      console.log('Cliente conectado');
    
      ws.on('message', (message) => {
        console.log(`Mensaje recibido: ${message}`);
        
        // Enviar el mensaje a todos los clientes conectados
        wss.clients.forEach((client) => {
          if (client.readyState === WebSocket.OPEN) {
            client.send(message.toString());
          }
        });
      });
    
      ws.on('close', () => {
        console.log('Cliente desconectado');
      });
    });
    
    This code sets up a WebSocket server integrated with an Express HTTP server running on port 8080. It allows real-time communication between the server and connected WebSocket clients. The server uses CORS middleware to handle cross-origin requests. When a client connects to the WebSocket server, a connection event is logged. The server listens for messages from the client, logs received messages, and broadcasts them to all connected clients that have an open WebSocket connection. It also logs when a client disconnects. This code facilitates bidirectional, real-time message distribution among multiple WebSocket clients.
    Lets dockerize the server, create ‘Dockerfile’:
    FROM node:14
    WORKDIR /usr/src/app
    COPY package*.json ./
    RUN npm install
    COPY . .
    EXPOSE 8080
    CMD ["node", "server.js"]
    This Dockerfile sets up a containerized environment for a Node.js application using the Node.js 14 image. It configures the working directory, copies application files and dependencies, installs the required Node.js packages, exposes port 8080 for the application, and specifies that server.js should run using Node.js when the container starts.
    Now is time to create docker image:
    docker build -t websocket-chat-server .

    Finally run the image:

    docker run -p 8080:8080 -d websocket-chat-server
    Screenshot

    For validating websocket server we will create an small html-javascript client:

    <!DOCTYPE html>
    <html>
    <body>
      <ul id="messages"></ul>
      <input type="text" id="messageInput" placeholder="Write a message">
      <button onclick="sendMessage()">Send</button>
    
      <script>
        const socket = new WebSocket('ws://localhost:8080');
    
        socket.onopen = function(event) {
          console.log('Setup connection', event);
        };
    
        socket.onmessage = function(event) {
          const messages = document.getElementById('messages');
          const li = document.createElement('li');
          li.textContent = event.data;
          messages.appendChild(li);
        };
    
        function sendMessage() {
          const input = document.getElementById('messageInput');
          const message = input.value;
          socket.send(message);
          input.value = '';
        }
      </script>
    </body>
    </html>

    This HTML code creates a basic web-based chat interface using WebSocket for real-time communication. It consists of an unordered list (<ul>) to display messages, an input field (<input>) for entering messages, and a «Send» button. The script establishes a WebSocket connection to a server at ws://localhost:8080. When the connection opens, a log message is displayed in the console. Incoming messages from the WebSocket server are dynamically added as list items (<li>) to the message list. When the «Send» button is clicked, the sendMessage function retrieves the user’s input, sends it to the server via the WebSocket, and clears the input field.

    Open file with your favourite browser:

    Screenshot

    Console log show that is properly connected and messages written are properly broadcasted

    websocket iOS Client

    We will follow the same design as we did with HTML and JavaScript:

    struct ContentView: View {
        @StateObject private var webSocketManager = WebSocketManager()
        @State private var messageText = ""
        
        var body: some View {
            VStack {
                List(webSocketManager.messages, id: \.self) { message in
                    Text(message)
                }
                
                HStack {
                    TextField("Enter message", text: $messageText)
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                    
                    Button("Send") {
                        webSocketManager.send(messageText)
                        messageText = ""
                    }
                }.padding()
            }
            .onAppear {
                webSocketManager.connect()
            }
        }
    }
    Code defines a ContentView that interacts with a WebSocket connection to display and send messages in a real-time chat interface. It uses a WebSocketManager (assumed to handle WebSocket connections and messaging) as a @StateObject, ensuring it persists across view updates. The body consists of a VStack with a List that dynamically displays messages received via the WebSocket, and an input section with a TextField for composing messages and a Button to send them. When the button is pressed, the typed message is sent via the webSocketManager, and the input field is cleared. The onAppear modifier ensures that the WebSocket connection is initiated when the view appears on screen.
    Finally WebSocketManager is where all magic takes place:
    class WebSocketManager: ObservableObject {
        private var webSocketTask: URLSessionWebSocketTask?
        @Published var messages: [String] = []
        
        func connect() {
            let url = URL(string: "ws://localhost:8080")!
            webSocketTask = URLSession.shared.webSocketTask(with: url)
            webSocketTask?.resume()
            receiveMessage()
        }
        
        func send(_ message: String) {
            webSocketTask?.send(.string(message)) { error in
                if let error = error {
                    print("Error sending message: \(error)")
                }
            }
        }
        
        private func receiveMessage() {
            webSocketTask?.receive { result in
                switch result {
                case .failure(let error):
                    print("Error receiving message: \(error)")
                case .success(let message):
                    switch message {
                    case .string(let text):
                        DispatchQueue.main.async {
                            self.messages.append(text)
                        }
                    default:
                        break
                    }
                    self.receiveMessage()
                }
            }
        }
    }
    The WebSocketManager class manages a WebSocket connection and handles sending and receiving messages. It uses URLSessionWebSocketTask to connect to a WebSocket server at a specified URL (ws://localhost:8080) and maintains an observable array of received messages, messages, for use in SwiftUI or other reactive contexts. The connect method establishes the connection and starts listening for incoming messages using the private receiveMessage method, which recursively listens for new messages and appends them to the messages array on the main thread. The send method allows sending a string message over the WebSocket, with error handling for failures. This class encapsulates WebSocket communication in a way that supports reactive UI updates.
    Finally, place both front ends (iPhone and web client) side by side. If you followed the instructions, you should have a chat between them.

    Conclusions

    WebSocket is a server technology, distinct from REST APIs or GraphQL, that is particularly well-suited for real-time, bidirectional communication. It’s ideal for applications that require fast, continuous interactions, such as real-time chats, online games, and collaborative tools (e.g., Figma, Google Docs). I hope you enjoyed reading this as much as I enjoyed writing and programming it.

    You can find the source code used for this post in the repository linked below.

    References

  • Crafting a Simple iOS App Using GraphQL APIs

    Crafting a Simple iOS App Using GraphQL APIs

    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