In the mobile native (iOS/Android) app production ecosystem, multiple frontend versions often coexist and interact with the same backend. Frontend updates are typically adopted gradually; while it's possible to enforce an update, this approach is generally considered disruptive and is used only in exceptional circumstances.
This post aims to demonstrate a method for controlling request responses based on the frontend version specified in the request. The backend implementation will use Vapor, and the frontend will be an iOS app. Links to the GitHub repositories hosting the source code are provided at the end of this post.
Keep request under control
Including the client’s frontend version in backend requests is crucial for several reasons:
Version-Specific Responses: The backend can tailor its responses to ensure compatibility and optimal functionality for each frontend version.
API Versioning: It helps the backend serve the appropriate API version, supporting backward compatibility while enabling updates and improvements.
Feature Support: Frontend versions may differ in their feature sets. The backend can adjust responses to include or exclude functionality based on the client’s capabilities.
Performance Optimization: Backend processing and payloads can be optimized for the specific requirements of each frontend version, improving system performance.
Error Handling: Knowing the frontend version allows for more relevant error messages and effective resolution of version-specific issues.
Security Enhancements: Version-specific security protocols or restrictions can be implemented, boosting system security.
By including the frontend version in client requests, developers can build robust, efficient, and maintainable systems that adapt to evolving requirements while maintaining compatibility with legacy clients.
Vapor backend
Vapor is an open-source web framework written in Swift, designed for building server-side applications. It offers a powerful and asynchronous platform for developing web applications, APIs, and backend services, all using Swift as the server-side language.
This post is not a “build your first server-side app” tutorial. However, don’t worry—at the end of the post, I’ll share the tutorials I followed to gain a deeper understanding of this technology.
To get started, we’ll create a new Vapor project. For this project, we won’t be working with databases, so you can safely answer “No” to all related prompts during the setup process.

We will create an endpoint specifically for checking the minimum required versions compatible with the backend and determining whether a forced update is necessary. The endpoint will use the GET method, and the path will be /minversion
.
struct MainController: RouteCollection {
func boot(routes: any Vapor.RoutesBuilder) throws {
let minversionRoutesGrouped = routes.grouped("minversion")
minversionRoutesGrouped.get(use: minVersion)
And the associated function to perform this will be as follows.
@Sendable
func minVersion(req: Request) async throws -> VersionResponse {
let currentVersion = "2.0.0"
let minimumVersion = "1.5.0"
let forceUpdate = true // o false dependiendo de la lógica de negocio
// Devuelve la respuesta como JSON
return VersionResponse(
currentVersion: currentVersion,
minimumVersion: minimumVersion,
forceUpdate: forceUpdate
)
}
Structure: We need to include the following information:
- Minimal Version: The minimum version of the application that the backend can handle.
- Current Version: The current version supported by the backend.
- Force Update: Whether a forced update is required.
Instructions:
Run the project, and check the log console to confirm that the server is ready.

Use the curl
command to call the specified endpoint.

The API returns a JSON object containing the minimum and current versions, as well as a force-update flag.
To simplify the backend’s ability to check frontend versions, we will add an additional attribute to each endpoint. This attribute will provide information about the frontend version. To illustrate this approach, we will create a sample POST endpoint that includes this feature.
struct MainController: RouteCollection {
func boot(routes: any Vapor.RoutesBuilder) throws {
let minversionRoutesGrouped = routes.grouped("minversion")
minversionRoutesGrouped.get(use: minVersion)
let sampleRoutesGrouped = routes.grouped("sample")
sampleRoutesGrouped.post(use: sample)
}
@Sendable
func sample(req: Request) async throws -> SampleResponse {
let payload = try req.content.decode(SampleRequestData.self)
let isLatestVersion = await payload.version == VersionResponse.current().currentVersion
let isForceUpdate = await VersionResponse.current().forceUpdate
guard isLatestVersion ||
!isForceUpdate else {
throw Abort(.upgradeRequired) // Force update flag set
}
guard await isVersion(payload.version, inRange: (VersionResponse.current().minimumVersion, VersionResponse.current().currentVersion)) else {
throw Abort(.upgradeRequired) // Version out of valid range
}
return SampleResponse(data: "Some data...")
}
The first thing the function does is validate that the version adheres to the X.Y.Z syntax.
struct SampleRequestData: Content {
let version: String
mutating func afterDecode() throws {
guard isValidVersionString(version) else {
throw Abort(.badRequest, reason: "Wrong version format")
}
}
private func isValidVersionString(_ version: String) -> Bool {
let versionRegex = #"^\d+\.\d+\.\d+$"#
let predicate = NSPredicate(format: "SELF MATCHES %@", versionRegex)
return predicate.evaluate(with: version)
}
}
Later on, the process involves validating the version of a client application against a server-defined versioning policy. If the version check is successful, a simple JSON response with sample data is returned.
Returning to the command line, we execute the sample using valid version values:

We received a valid sample endpoint response, along with current, minimum version and wether forced update is being required.
However, when we set a version lower than the required minimum, we encountered an error requesting an upgrade.

While the implementation is theoretically complete, handling version updates on the front end is not difficult, but any mistakes in production can have dramatic consequences. For this reason, it is mandatory to implement a comprehensive set of unit tests to cover the implementation and ensure that when versions are updated, consistency is maintained.

From now on, every new endpoint implemented by the server must perform this frontend version check, along with other checks, before proceeding. Additionally, the code must be data race-safe.

At the time of writing this post, I encountered several issues while compiling the required libraries for Vapor. As a result, I had to revert these settings to continue writing this post. Apologies for the back-and-forth.
IOS frontend
The iOS app frontend we are developing will primarily interact with a sample POST API. This API accepts JSON data, which includes the current frontend version.
- If the frontend version is within the supported range, the backend responds with the expected output for the sample POST API, along with information about the versions supported by the backend.
- If the frontend version falls below the minimum supported version and a forced update is required, the backend will return an “update required” error response.
To ensure compliance with Swift 6, make sure that Strict Concurrency Checking is set to Complete.

… and Swift language version to Swift 6.

Before we start coding, let’s set the app version. The version can be defined in many places, which can be quite confusing. Our goal is to set it in a single, consistent location.

This is the unique place where you need to set the version number. For the rest of the target, we will inherit that value. When we set the version in the target, a default value (1.0) is already set, and it is completely isolated from the project. We are going to override this by setting MARKETING_VERSION to $(MARKETING_VERSION), so the value will be taken from the project’s MARKETING_VERSION.

Once set, you will see that the value is adopted. One ring to rule them all.
The application is not very complicated, and if you’re looking for implementation details, you can find the GitHub repository at the end of the post. Essentially, what it does is perform a sample request as soon as the view is shown.
Make sure the Vapor server is running before launching the app on a simulator (not a real device, as you’re targeting localhost). You should see something like this:

The current app version is 1.7.0, while the minimum supported backend version is 1.5.0, and the backend is currently at version 2.0.0. No forced update is required. Therefore, the UI displays a message informing users that they are within the supported version range, but it also indicates that an update to the latest version is available.
Once we configure the Vapor backend to enforce a forced update:
let versionResponse = VersionResponse(currentVersion: "2.0.0",
minimumVersion: "1.5.0",
forceUpdate: true)
Re-run vapor server:

Re-run the app:

The front-end needs to be updated, and users are required to update the app. Please provide a link to the Apple Store page for downloading the update.
Conclusions
References
- Building a Vapor backend
NSScreencast