Tag: Server Side Swift

  • Swift Vapor: How to run a scheduled job

    Step 1. Install Redis on your Mac

    Follow the official guide

    https://redis.io/docs/latest/operate/oss_and_stack/install/archive/install-redis/install-redis-on-mac-os/

    brew install redis
    
    brew services start redis
    
    brew services info redis
    
    redis-cli
    screenshot 2025 12 28 at 11.38.31 am

    Step 2. Setup Redis Configuration in Vapor App

    Add Redis Swift Package

    .package(url: "https://github.com/vapor/queues-redis-driver.git", from: "1.0.0")

    // swift-tools-version:6.0
    import PackageDescription
    
    let package = Package(
        name: "TestServer",
        platforms: [
            .macOS(.v13)
        ],
        dependencies: [
            // 💧 A server-side Swift web framework.
            .package(url: "https://github.com/vapor/vapor.git", from: "4.111.0"),
            .package(url: "https://github.com/vapor/fluent", from: "4.12.0"),
            .package(url: "https://github.com/vapor/fluent-sqlite-driver", from: "4.8.0"),
            .package(url: "https://github.com/vapor/sql-kit", from: "3.33.2"),
            .package(url: "https://github.com/lukaskubanek/LoremSwiftum", from: "2.2.3"),
            .package(url: "https://github.com/vapor/fluent-postgres-driver", from:"2.10.0"),
            .package(url: "https://github.com/vapor/jwt", from: "5.1.2"),
            .package(url: "https://github.com/vapor/queues-redis-driver.git", from: "1.0.0"),
        ],
        targets: [
            .target(
                name: "App",
                dependencies: [
                    .product(name: "Vapor", package: "vapor"),
                    .product(name: "Fluent", package: "fluent"),
                    .product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"),
                    .product(name: "SQLKit", package: "sql-kit"),
                    .product(name: "LoremSwiftum", package: "LoremSwiftum"),
                    .product(name: "JWT", package: "jwt"),
                    .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"),
                    .product(name: "QueuesRedisDriver", package: "queues-redis-driver")
                ],
                swiftSettings: [
                    // Enable better optimizations when building in Release configuration. Despite the use of
                    // the `.unsafeFlags` construct required by SwiftPM, this flag is recommended for Release
                    // builds. See <https://github.com/swift-server/guides#building-for-production> for details.
                    .unsafeFlags(["-cross-module-optimization"], .when(configuration: .release))
                ]
            ),
            .executableTarget(name: "Run", dependencies: [
                    .target(name: "App")
                ]
            ),
            .testTarget(name: "AppTests", dependencies: [
                .target(name: "App"),
                .product(name: "XCTVapor", package: "vapor")
            ])
        ]
    )
    

    Create an AsyncScheduledJob

    import Foundation
    import Vapor
    import Queues
    
    struct ScheduledJobs: AsyncScheduledJob {
        // Add extra services here via dependency injection, if you need them.
        func run(context: QueueContext) async throws {
            context.logger.info("Starting ScheduledJobs")
            print("✅ It is called")
            //Call other services using context.application.client
    //        context.application.client
            context.logger.info("ScheduledJobs completed")
        }
    }
    

    Register Scheduled Job in Vapor App

    https://docs.vapor.codes/advanced/queues/#available-builder-methods

    import Vapor
    import QueuesRedisDriver
    
    public func configure(_ app: Application) throws {
        let redisConfig = try RedisConfiguration(
            hostname: "127.0.0.1",
            port: 6379,
            pool: .init(
                maximumConnectionCount: .maximumActiveConnections(50),
                minimumConnectionCount: 10
            )
        )
        app.redis.configuration = redisConfig
        //Use Redis
        app.queues.use(.redis(redisConfig))
        
        //Register ScheduledJob - It runs every 30 seconds
        app.queues.schedule(ScheduledJobs())
            .minutely()
            .at(30)
        
        try app.queues.startScheduledJobs()
    
    ....
    }
    screenshot 2025 12 28 at 11.51.30 am

    Conclusion

    Hope my articles helps you who want to run scheduled job using Redis. Please like my post or leave a comment it helps me continue share my knowledge for free.

  • Server-Side Swift – AWS Lambda with OpenAPI Generator Part 1

    screenshot 2025 10 26 at 10.37.10 pm

    This post is for whom want to use AWS Lambda with OpenAPI Generator. Official guide is useful but I felt there are some missing information. So I wrote this post. You can successfully run AWS Lambda function on your local machine and debug your code.

    Let’s start from very simple example. You only need 4 files. I’ll explain details. (Ignore Tests folder, no need it in this tutorial)

    • Package.swift
    • NativeMobileServer.swift
    • openapi.yaml
    • openapi-generator-config.yaml
    screenshot 2025 10 26 at 10.49.32 pm

    Step 1. Define OpenAPI Spec

    This OpenAPI spec is for tutorial.

    Please check folder and file structure. Create an openapi.yaml

    openapi: 3.1.0
    info:
      title: MobileJobService
      version: 1.0.0
    
    paths:
      /jobs/fetch:
        post:
          summary: Fetch job data from external source
          description: >
            This endpoint is called by a scheduled Lambda or backend service.
            It fetches job data from the given URL and processes it using the provided prompt.
          operationId: fetchJobs
          tags:
            - jobs
          requestBody:
            required: true
            content:
              application/json:
                schema:
                  $ref: '#/components/schemas/FetchJobsRequest'
          responses:
            '200':
              description: Successfully fetched and processed job data
              content:
                application/json:
                  schema:
                    $ref: '#/components/schemas/JobListResponse'
            '400':
              description: Invalid input parameters
            '500':
              description: Internal error during job fetch or processing
    
    components:
      schemas:
        FetchJobsRequest:
          type: object
          required:
            - url
            - prompt
          properties:
            url:
              type: string
              format: uri
              description: Target URL to scrape or fetch job data from
            prompt:
              type: string
              description: Instruction / extraction prompt used to parse the fetched page
    
        JobListResponse:
          type: array
          items:
            $ref: '#/components/schemas/Job'
    
        Job:
          type: object
          required:
            - id
            - title
          properties:
            id:
              type: string
              description: Unique identifier for the job
            title:
              type: string
              description: Job title
            country:
              type: string
              description: Country code (e.g. SG, US, TW)
            city:
              type: string
              description: City name (e.g. Singapore)
            postedAt:
              type: string
              format: date-time
              description: When this job was posted, if known
            company:
              type: string
              description: Company name
            team:
              type: string
              description: Team / department (e.g. Mobile, Backend, Growth)
            jobDescriptionLink:
              type: string
              format: uri
              description: Public link to full job description
            jobApplyLink:
              type: string
              format: uri
              description: Public link to apply
            salary:
              $ref: '#/components/schemas/Salary'
            description:
              type: string
              description: Cleaned / extracted full-text description for the role
    
        Salary:
          type: object
          properties:
            min:
              type: number
              description: Minimum compensation
            max:
              type: number
              description: Maximum compensation
            basis:
              type: string
              enum: [year, month]
              description: Salary period basis (yearly or monthly)
    

    And then create openapi-generator-config.yaml

    generate:
      - types
      - server
    

    Step 2. Create Swift Package Manger

    Package.swift

    // swift-tools-version: 6.2
    // The swift-tools-version declares the minimum version of Swift required to build this package.
    
    import PackageDescription
    
    let package = Package(
        name: "NativeMobileServer",
        platforms: [
            .macOS(.v15)
        ],
        products: [
            .executable(name: "NativeMobileServer", targets: ["NativeMobileServer"])
        ],
        dependencies: [
            .package(url: "https://github.com/apple/swift-openapi-generator.git", from: "1.10.3"),
            .package(url: "https://github.com/apple/swift-openapi-runtime.git", from: "1.8.2"),
    
            .package(url: "https://github.com/awslabs/swift-aws-lambda-runtime.git", from: "2.0.0"),
            .package(url: "https://github.com/awslabs/swift-aws-lambda-events.git", from: "1.2.0"),
            .package(url: "https://github.com/awslabs/swift-openapi-lambda.git", from: "2.0.0"),
    
            .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.0.0"),
        ],
        targets: [
            .executableTarget(
                name: "NativeMobileServer",
                dependencies: [
                    .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
    
                    .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"),
                    .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"),
    
                    .product(name: "OpenAPILambda", package: "swift-openapi-lambda"),
                    .product(name: "ServiceLifecycle", package: "swift-service-lifecycle"),
                ],
                path: "Sources/NativeMobileServer",
                resources: [
                    .copy("openapi.yaml"),
                    .copy("openapi-generator-config.yaml")
                ],
                plugins: [
                    .plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator")
                ]
            ),
            .testTarget(
                name: "NativeMobileServerTests",
                dependencies: ["NativeMobileServer"]
            ),
        ]
    )
    

    You can remove .testTarget if you don’t need to write test cases.

    Step 3. Write a main function

    //
    //  NativeMobileServer.swift
    //  NativeMobileServer
    //
    //  Created by Shawn Sungwook Baek on 10/26/25.
    //
    
    import Foundation
    import Logging
    import OpenAPILambda
    import OpenAPIRuntime
    
    @main
    struct JobServiceImpl: APIProtocol, OpenAPILambdaHttpApi {
        func fetchJobs(_ input: Operations.fetchJobs.Input) async throws -> Operations.fetchJobs.Output
        {
            logger.info("fetchJobs invoked")
            let requestBody = input
           
            if case let .json(request) = requestBody.body {
                logger.info("Fetching jobs from \(request.url) with prompt: \(request.prompt)")
            }
    
            let mockJobs: [Components.Schemas.Job] = [
                .init(
                    id: "job-001",
                    title: "iOS Developer",
                    country: "SG",
                    postedAt: Date(),
                    company: "Apple",
                    team: "Mobile",
                    jobDescriptionLink: "https://example.com/job/1",
                    jobApplyLink: "https://example.com/apply/1",
                    salary: .init(min: 6000, max: 9000, basis: .month),
                    description: "Develop and maintain the iOS application."
                ),
                .init(
                    id: "job-002",
                    title: "Backend Engineer",
                    country: "TW",
                    postedAt: Date(),
                    company: "Uber",
                    team: "Server",
                    jobDescriptionLink: "https://example.com/job/2",
                    jobApplyLink: "https://example.com/apply/2",
                    salary: .init(min: 7000, max: 10000, basis: .month),
                    description: "Build API services and integrations."
                ),
            ]
            return .ok(.init(body: .json(mockJobs)))
        }
    
        let logger: Logger
    
        func register(transport: OpenAPILambdaTransport) throws {
            try transport.router.get("/health") { _, _ in
                "OK"
            }
            logger.trace("Available Routes\n\(transport.router)")
            // to log all requests and their responses, add a logging middleware
            let loggingMiddleware = LoggingMiddleware(logger: logger)
    
            // MANDATORY (middlewares are optional)
            try self.registerHandlers(on: transport, middlewares: [loggingMiddleware])
        }
    
        static func main() async throws {
            let openAPIService = JobServiceImpl(loggerLabel: "JobService")
            try await openAPIService.run()
    
        }
    
        init(loggerLabel: String) {
            var logger = Logger(label: loggerLabel)
            logger.logLevel = .trace
            self.logger = logger
        }
    }
    

    Step 4. Check Scheme in Xcode

    screenshot 2025 10 26 at 10.52.56 pm

    Check Executable Target.

    screenshot 2025 10 26 at 10.54.39 pm

    Important!

    You need to set Environment Variables

    • LOCAL_LAMBDA_SERVER_ENABLED
    • LOCAL_LAMBDA_PORT <- Optional! Default port is 7000

    Try build and run and check which pid is using 7000 port.

    lsof -i :7000

    If other program uses that port, you can change default port by setting LOCAL_LAMBDA_PORT

    screenshot 2025 10 26 at 10.57.10 pm

    When you run it, you will see the message. (In my case, I changed the default port 8000)

    Step 5. Invoke Lambda function

    This part you may confusing because you can’t call lambda like

    curl -v -X POST http://127.0.0.1:8000/jobs/fetch \
      -H "Content-Type: application/json" \
      -d '{
        "url": "https://example.com/jobs",
        "prompt": "Extract latest mobile job listings"
      }'
    screenshot 2025 10 26 at 11.01.22 pm

    If you want to see more detail information, check this out

    To invoke and debug our code, we should invoke function like this.

    If you want to learn more about this payload structure v2.0 in API Gateway check here

    curl -X "POST" "http://127.0.0.1:8000/invoke" \
         -H 'Content-Type: application/json; charset=utf-8' \
         -d $'{
      "requestContext": {
        "accountId": "",
        "time": "",
        "http": {
          "path": "/jobs/fetch",
          "userAgent": "",
          "method": "POST",
          "protocol": "HTTP/1.1",
          "sourceIp": "127.0.0.1"
        },
        "domainName": "",
        "timeEpoch": 0,
        "domainPrefix": "",
        "apiId": "",
        "requestId": "",
        "stage": "$default"
      },
      "rawPath": "/jobs/fetch",
      "rawQueryString": "",
      "version": "2.0",
      "routeKey": "$default",
      "isBase64Encoded": true,
      "body": "{\\"url\\":\\"https://jobs.apple.com/en-sg/search\\", \\"prompt\\":\\"Extract latest mobile job listings\\"}"
    }'

    I’ve set breakpoint at here.

    screenshot 2025 10 26 at 11.07.43 pm

    Let’s invoke function.

    screenshot 2025 10 26 at 11.09.43 pm

    Ok, breakpoint is working.

    screenshot 2025 10 26 at 11.09.04 pm

    And I can see the results from AWS Lambda function

    Conclusion

    Swift is very powerful for developing server-side applications. There a lot of great open source projects. In Part 2, I’ll explain how to deploy AWS Lambda Swift function to the AWS using SAM CLI.

  • Do you require a server for your iOS app? CloudKit might be the perfect fit for you!

    Do you require a server for your iOS app? CloudKit might be the perfect fit for you!

    Choosing a server can be challenging, especially if you’re not a backend engineer but still need one for your iOS app. You may find yourself in this situation.

    So, Why did I consider CloudKit?

    Well, first off, it’s maintained by Apple and has been around since its introduction at the 2014 WWDC.

    Apple actively uses it for various apps like Photos and Notes, which gives me confidence in its longevity and reliability. Plus, I expect it to receive regular updates from Apple.

    My experience

    Parse.com

    I used Parse (Acquired by Facebook) and contributed to the Parse iOS SDK. I liked it, but Facebook announced its discontinuation.

    Consequently, I had to migrate to alternatives like back4app or Sashido.io, which serve as alternatives to parse.com. While they were good, I encountered issues with the lack of active updates to the iOS SDK. This led me to contribute to its development, but I felt it was a waste of time. I wanted to focus on my app rather than on open-source projects.

    Vapor

    I like Vapor because I can create a server using Swift, which is a significant benefit as an iOS Engineer. Additionally, there are numerous helpful resources available, such as books, YouTube videos, and Udemy courses.

    But why did I consider CloudKit?

    Unlike CloudKit, I would need to implement server logic from scratch, including authentication, deployment, database setup, migration, and data sharing between users.

    My approach

    screenshot 2024 02 12 at 4.14.49e280afpm

    Context

    • I’ve already implemented a server using Vapor to handle authentication. While working on sharing data between users, I researched an easy way to implement the invite/accept feature and found that CloudKit fully supports it.

    The server I implemented using Vapor is deployed on AWS, using ECS with a Load Balancer. Its primary role is handling SignIn/SignUp and supporting user-related features such as changing passwords and usernames. All user information is saved in PostgreSQL.

    For data synchronization across user devices, I utilize NSPersistentCloudKitContainer, which syncs data between CoreData and CloudKit seamlessly. This integration is very convenient and eliminates the need for manual synchronization logic.

    CloudKit also supports invite and accept functionality for sharing data between users, eliminating the need to implement server-side logic for this feature.

    Be aware of CloudKit topics

    Now, returning to Vapor,

    I can access and edit CloudKit’s data using CloudKit Web Service. NSPersistentCloudKitContainer use a special zone called com.apple.coredata.cloudkit.zone. If you’re interested how to access it using CloudKit Web Service, you can check out Reading CloudKit Records for Core Data.

    Public Data

    As for public data storage, I haven’t decided yet where to store it. I’ll update you once I make a decision.

    How about CloudKit costs?

    Currently, the pricing seems like a black box. I can’t find pricing information on the Apple Developer website. That’s why I can’t decide where I should store public data.

    If you want to know AWS costs, see my previous post.

    I captured pricing information by accessing the archived website.

    screenshot 2024 02 12 at 2.15.25e280afpm
    screenshot 2024 02 12 at 2.15.42e280afpm
    screenshot 2024 02 12 at 2.14.45e280afpm

    Overage Fees

    Asset Storage $0.03/GB
    Database Storage $3.00/GB
    Data Transfer $0.10/GB
    Requests per sec $100 per 10 requests
  • AWS Hosting Costs for Vapor Swift Server – Monthly $65 Breakdown

    AWS Hosting Costs for Vapor Swift Server – Monthly $65 Breakdown

    I’ve been using AWS to host my Vapor Swift server, but currently, there’s no traffic as I am just running the server 24/7.

    Monthly Costs:

    The total cost is approximately $65 per month, with the most expensive service being RDB (using PostgreSQL). Here’s a list of the AWS services I’m using:

    • EC2 T2.nano Instance for AWS Cloud9

    • RDB (PostgreSQL)

    • Elastic Load Balancer

    • ECS Fargate, CPU: .25 vCPU, Memory: .5GB (500MB)

    f2068 86563 screenshot2023 02 19at8.39.12am

     

    Breakdown of Service Costs:

    00e80 91aac screenshot2023 02 19at8.39.46am

    Conclusion

    I’m sharing the expenses with two friends, so it’s not too burdensome for me at the moment. However, I’m considering turning off the EC2-Other (Cloud 9 Service) as I only use it to access the terminal for checking database tables.

    Despite this, I plan to continue using AWS as switching to another service would be time-consuming. This year, my focus is on monetizing my app through in-app purchases or Adsense. Hopefully, this will cover my server hosting costs.