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

Copyright © 2024-2025 JaviOS. All rights reserved