For most of SafiTrack's life, our mobile app talked to the same REST API as everything else. That was fine when the app was thin. A few screens, mostly read-heavy, occasional writes. It stopped being fine when we started building the new mobile experience we wrote about last year.
The core problem was over-fetching. A rep opening the dashboard on mobile would trigger six separate network requests, each returning more data than the screen actually needed. On a decent LTE connection this was merely inefficient. On the patchy rural networks our field reps actually work in, it was a real usability problem.
Why REST was the wrong shape for our mobile data needs
Consider what the visit log screen needs: the rep's name and avatar, a list of today's visits with customer name, address, time, and status, and a count of pending tasks. That data lives across four different REST resources. /users/me, /visits, /customers, /tasks. Each request is independently paginated, requires its own error handling, and has to be joined on the client.
We tried solving this with purpose-built endpoint aggregates. A /dashboard-summary endpoint that returned everything the dashboard needed in one call. That worked, but it created a new problem: every time the dashboard UI changed, the backend team had to update the aggregate endpoint. The frontend and backend were tightly coupled through a growing collection of special-purpose endpoints that didn't compose and couldn't be reused.
The migration approach
We didn't do a big bang rewrite. We introduced GraphQL as a separate layer in front of the existing REST services and migrated screens one at a time, keeping REST available as the fallback throughout. This let us ship incrementally and roll back individual screens without touching anything else.
The GraphQL server is a thin translation layer. It receives queries, fans them out to the relevant REST services (or directly to the database for the hot paths we've since optimised), assembles the response, and returns it. We're using Apollo Server, which was the obvious choice given our Node.js backend, and the integration with our existing auth middleware was straightforward.
# Example: what the dashboard query looks like now
query DashboardSummary {
me {
name
avatarInitials
}
todaysVisits {
id
customer { name address }
scheduledAt
status
}
pendingTaskCount
}
One request, exactly the fields the screen needs, no client-side joining. On a slow connection, the dashboard load time dropped by just over half.
Where we got it wrong: N+1 queries
The N+1 problem in GraphQL is well-documented but we still walked straight into it. Our initial todaysVisits resolver fetched the visit list, then for each visit made a separate call to resolve the customer. Fifty visits meant fifty customer fetches. We caught it in staging when we looked at the query logs and saw the volume of database calls and fixed it with DataLoader (Facebook's batching utility) but it added a sprint's worth of work we hadn't planned for.
If you're doing this migration: set up query complexity analysis and resolver-level logging before you ship to production, not after. The problems are invisible until they're not.
What we got right: schema-first design
We wrote the schema before writing any resolvers, and we treated the schema review as a product conversation, not just an engineering one. The product manager and mobile lead both sat in the schema design sessions, which meant we caught several cases where the shape of the data we were planning to expose didn't match how the screens actually needed to use it.
This sounds obvious but it's the opposite of how REST API design usually goes. Where the backend exposes what it has and the frontend adapts. Forcing the shape conversation upfront saved us significant rework.
The verdict after six months
We're not moving the web client to GraphQL. The REST API works fine for our dashboard and the overhead of maintaining both isn't worth it for screens that aren't network-constrained. GraphQL is mobile-only for now and will probably stay that way.
If you're considering the same migration: the benefits are real on mobile, the complexity is real everywhere, and the N+1 problem will get you if you're not watching for it. Go schema-first, instrument everything, and don't try to migrate everything at once.