Category: Server Side Swift

Post about 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

    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()
    
    ....
    }

    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

    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

    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

    Check Executable Target.

    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

    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"
      }'

    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.

    Let’s invoke function.

    Ok, breakpoint is working.

    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.

  • How to Validate an OpenAPI Spec Using IBM OpenAPI Validator

    How to Validate an OpenAPI Spec Using IBM OpenAPI Validator

    This post is the second part of our OpenAPI series. Let’s set up GitHub Actions for validating an OpenAPI spec before merging into the main branch.

    Check more details

    Step 1. Install IBM OpenAPI Validator

    npm i -g ibm-openapi-validator
    npm i @ibm-cloud/openapi-ruleset

    Step 2. Setup OpenAPI Rullset

    extends: '@ibm-cloud/openapi-ruleset'
    rules:
      ibm-accept-and-return-models: info
      ibm-integer-attributes: false
      ibm-required-array-properties-in-response: false
      ibm-property-casing-convention: false
    

    Create an file – validator-rules.yaml

    You can check details

    ibm rules (inherent from spectral:as rules)

    spectral:oas rules

    Step 3. Setup Configurations

    Create an file – validator-config.yml

    errorsOnly: true
    colorizeOutput: true
    limits:
      warnings: 25
    outputFormat: 'text'
    summaryOnly: false
    files:
      - api.yml
    ignoreFiles:
      - validator-config.yml
    logLevels:
      root: error
      ibm-schema-description-exists: debug
    ruleset: ./validator-rules.yaml
    produceImpactScore: false
    markdownReport: true
    

    Check document

    Step 4. Check Project Folders and Files

    Step 5. Run Validator on your Local Machine

    lint-openapi -c ./validator-config.yml ./api.yml --errors-only --no-colors
    

    After run the command, It will generate reporting file (md)

    • api-validator-report.md

    Step 6. Add .gitignore file

    node_modules/
    package-lock.json
    package.json
    api-validator-report.md

    Step 7. Setup Github Action

    name: Validate OpenAPI Documentation
    on:
      pull_request:
        branches: [main]
      push:
        branches: [main]
    
    jobs:
      validate:
        name: Validate OpenAPI Documentation
        runs-on: ubuntu-latest
        permissions:
          contents: read
          pull-requests: write
    
        steps:
          - name: Checkout code
            uses: actions/checkout@v4
    
          - name: Setup Node.js
            uses: actions/setup-node@v4
            with:
              node-version: '20'
    
          - name: Install IBM OpenAPI Validator
            run: |
              npm i -g ibm-openapi-validator
              npm i @ibm-cloud/openapi-ruleset
    
          - name: Validate api.yml
            id: validation
            run: |
              if lint-openapi -c ./validator-config.yml ./api.yml --errors-only --no-colors > validation_result.txt 2>&1; then
                echo "validation_status=success" >> $GITHUB_OUTPUT
              else
                echo "validation_status=failed" >> $GITHUB_OUTPUT
              fi
            continue-on-error: true
    
          - name: Comment PR with validation output
            if: github.event_name == 'pull_request'
            uses: actions/github-script@v7
            with:
              script: |
                const fs = require('fs');
                const validationStatus = '${{ steps.validation.outputs.validation_status }}';
                const validationOutput = fs.readFileSync('api-validator-report.md', 'utf8');
                  
                let body = '';
                
                if (validationStatus === 'success') {
                  body = `## ✅ OpenAPI Validation Passed
                  
                  **File**: \`./api.yml\`
                  **Commit**: \`${{ github.sha }}\`
                  
                  ${validationOutput}
                  
                  `;
                } else {
                  body = `## ❌ OpenAPI Validation Failed
                                
                  **File**: \`./api.yml\`
                  **Commit**: \`${{ github.sha }}\`
                  
                  ${validationOutput}
                  
                  `;
                }
                
                // Truncate if too long for GitHub comment limit
                if (body.length > 65000) {
                  body = body.slice(0, 65000) + '\n\n... (truncated due to GitHub comment length limit)';
                }
                
                // Find existing comment and update or create new one
                const { data: comments } = await github.rest.issues.listComments({
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  issue_number: context.issue.number,
                });
                
                const marker = 'OpenAPI Validation';
                const existing = comments.find(c => c.body && c.body.includes(marker));
                
                if (existing) {
                  await github.rest.issues.updateComment({
                    owner: context.repo.owner,
                    repo: context.repo.repo,
                    comment_id: existing.id,
                    body
                  });
                } else {
                  await github.rest.issues.createComment({
                    owner: context.repo.owner,
                    repo: context.repo.repo,
                    issue_number: context.issue.number,
                    body
                  });
                }
    
    
          - name: Fail job if validation failed
            if: steps.validation.outputs.validation_status == 'failed'
            run: exit 1
    

    Create Github Action yml file

    • .github/workflows/validator.yml

    ⚠️ Check md file name – api-validator-report.md

    const validationOutput = fs.readFileSync('api-validator-report.md', 'utf8');

    Step 8. Check Github Action’s results

    This is the last step, let’s check the results.

    When you create a PR that targets main branch, our GitHub action will run and comment results on your Opened PR.

    IBM OpenAPI Validator provides very detailed information. To fix issues, You can update your OpenAPI Spec file or You can change the rulesets if you don’t want to change your OpenAPI Spec files.

    BTW This validator is really helpful it prevent wrong OpenAPI spec merged into main branch.

    Related Posts