Engineering article
Moving FoodieCalc's FatSecret integration behind a Vapor microservice
Why FoodieCalc moved FatSecret search out of the iOS client, what changed in the architecture, and what I learned taking a SwiftUI app into backend development.

Vapor microservice
FoodieCalc reached one of those milestones that changes how the app feels as a product, even if the user interface barely changes.
The app now has its own Vapor microservice.
That service sits between the iOS client and the FatSecret Platform API. Food search now goes through https://api.itworksonmymac.com/foods/search, the FatSecret client secret is no longer part of the app source, and OAuth token handling is completely server-side.
For a nutrition app, this is not glamorous work. It is not a new chart, widget, mascot, or onboarding screen. But for production readiness, it was huge.
The original problem
FoodieCalc uses FatSecret for nutrition search. The first implementation worked like many early mobile integrations:
- the app knew how to request a FatSecret OAuth token,
- the app used that token to call FatSecret food search,
- the feature modules kept using the same
FCFoodSearchResponsemodels, - and the user got the right search results.
Technically, it worked.
But “it works” is not enough when secrets are involved.
An iOS app binary is not a secure place for long-lived API credentials. Even if the source code is private and even if the UI feels polished, anything shipped to the client should be treated as observable. The permanent FatSecret credential needed to leave the app.
That was the security requirement: no FatSecret client secret in the iOS client, no Basic auth in the app, and no mobile-side OAuth machinery that depends on a permanent secret.
FatSecret also has account-level settings such as IP restrictions, and their public Postman getting-started material notes that OAuth requests depend on allowed source IP configuration. That made the server boundary feel even more natural: a stable backend host can own the credential and the upstream relationship.
The first version of the idea
The first plan was intentionally small: create a token-only proxy.
The app would call:
GET /token
The Vapor service would hold:
FATSECRET_CLIENT_ID
FATSECRET_CLIENT_SECRET
APP_SHARED_KEY
Then the server would perform the FatSecret client-credentials exchange and return only:
{
"access_token": "...",
"expires_in": 86400
}
That was already a big improvement. The permanent secret would leave the app, and the stealable value would become a temporary bearer token.
But there was still a caveat: the iOS app would still receive a FatSecret bearer token and call FatSecret directly.
For a first pass that can be acceptable. For FoodieCalc, it felt like stopping halfway.
The real milestone: proxying food search
The final implementation went further.
Instead of giving the app a token, FoodieCalc now proxies the authenticated food search itself:
iOS app
-> GET https://api.itworksonmymac.com/foods/search
-> Vapor service validates X-App-Key
-> Vapor service gets or refreshes FatSecret token
-> Vapor service calls FatSecret foods/search/v5
-> Vapor service returns the FatSecret JSON body unchanged
That last part matters. The server returns the same shape the app already knows how to decode, so the client feature code did not need a dramatic rewrite.
The iOS side changed at the networking boundary:
FCFoodSearchServiceImpnow points to the FoodieCalc API endpoint.- The default session sends
X-App-Key. - Query items stayed the same.
FCFoodSearchResponsestayed the same.- The old
FCAuthService,FCSessionImp,FCOAuthResponse, and launch-time token warmup were removed.
That is the kind of change I like: the app becomes safer, but the feature modules remain calm.
Why Vapor made sense
I chose Vapor because it let the backend stay in Swift. Vapor is a server-side Swift framework for APIs and HTTP servers, so the mental model did not feel like jumping to a completely different world.
There was still plenty to learn.
Backend work forced me to think about things that are easy to ignore inside an app target:
- environment variables instead of bundled configuration,
- health endpoints,
- server logs,
- upstream error mapping,
- container builds,
- VPS deployment,
- reverse proxying,
- TLS,
- and how to recover when something fails outside Xcode.
That was uncomfortable in the good way. It made the product more real.
The server shape
The service is intentionally small.
It has a protected food search endpoint:
GET /foods/search
The request accepts the same search concepts the app already used:
search_expression
max_results
page_number
The server validates X-App-Key, then calls FatSecret’s foods/search/v5 endpoint with a bearer token that never leaves the backend.
The token is owned by a server-side token service. If FatSecret responds with 401, the server refreshes the token and retries once. That retry logic used to belong to the client-side networking world. Moving it server-side simplified the iOS app and made failures easier to reason about.
The cache changed the rate-limit story
The security improvement was the main requirement, but the proxy unlocked another important advantage: caching.
FatSecret quota is account-based. Creating more tokens does not magically create more quota. What matters is the number of upstream API calls.
When every device calls FatSecret directly, every search hits the upstream API:
1000 users search "chicken" = 1000 upstream requests
With the Vapor proxy, the server can cache successful search responses:
1000 users search "chicken" = 1 upstream request, then cache hits
Food search data is a good fit for caching because it is relatively static. FoodieCalc uses a 24-hour in-memory cache with a bounded size. It only caches successful FatSecret search bodies, never error responses.
That one backend decision gives the app a much better chance of staying healthy as usage grows.
Deployment made it feel real
The service is deployed to a VPS as foodiecalc-api.
The production shape is:
- Vapor app in a container image,
- Docker Engine on the Linux VPS,
- Docker Compose for the service stack,
- Caddy as the reverse proxy,
- automatic HTTPS certificates,
- environment variables stored only on the server,
- and the public endpoint at
api.itworksonmymac.com.
Coming from iOS, this was a different kind of shipping. There is no App Store Connect screen, no simulator, no TestFlight build. There is a server, logs, DNS, TLS, ports, firewall rules, and a health endpoint.
It made me respect backend work more.
What improved
This milestone changed FoodieCalc in several ways:
- The FatSecret client secret is no longer in the app source.
- The iOS app never receives FatSecret bearer tokens.
- FatSecret OAuth is fully server-side.
- Food search runs through a private API boundary.
- Search responses can be cached server-side.
- Repeated identical searches reduce upstream calls.
- The client networking layer is simpler.
- The backend can evolve into a fuller API layer later.
The app experience remains the same, which is exactly the point. A user searches for food and gets results. The difference is that the product is now built on a safer foundation.
What I learned
The biggest lesson was that backend development is not just “mobile, but somewhere else.”
In iOS, I spend a lot of energy on state, navigation, persistence, and user experience. In this microservice, the important questions were different:
- Where should secrets live?
- What can the client be trusted with?
- Which failures should be retried?
- What should be cached?
- What should stay stateless?
- How do I deploy the same thing again tomorrow?
- What happens if the upstream API changes?
Those are product questions as much as engineering questions.
FoodieCalc started as a nutrition tracker for my own goals. This milestone made it feel like a real system: iOS app, backend service, third-party API, VPS, deployment workflow, and a security boundary I can keep improving.
That is exactly the kind of journey I wanted IT Works On My Mac to document.