GraphQL or REST is a question about how much is written down.
REST is HTTP, and HTTP writes down which requests are safe to repeat and whose answers a cache is allowed to keep. GraphQL is a query language and an execution engine, and its specification deliberately leaves transport to whoever implements it. The word HTTP appears exactly once in the whole September 2025 edition, in a passing note about serializing variables. So this is not a speed question. It is a question about which operational rules you inherit and which ones become yours. Both styles are ordinary network calls in a React Native and Expo app. Every claim below was read from the GraphQL specification and from MDN's HTTP method reference on 17 October 2026. If the project in front of you is new to you, the getting started with Expo walkthrough covers the ground under it.
What follows is what each document actually promises, which of the two your app can cache, how a failure reaches your code in each one, and what happens to both when the API changes under a version already on somebody's phone. That last one is the mobile problem, and it is the one most comparisons skip.
Start hereThe short version
One of them hands you rules. The other hands you a schema.
Choose REST and you are choosing HTTP, which already says that a GET is safe, idempotent and cacheable, and that a POST is none of those by default. Those sentences are the reason a proxy, a CDN or a phone's own HTTP stack can reuse a response without asking you. You get that for nothing, and you also get a payload shaped by whoever wrote the endpoint rather than by the screen displaying it.
Choose GraphQL and you are choosing a type system and a client-specified response. The schema is machine readable, the client asks for the fields it wants, and the service answers with those fields and nothing else. In return, every operational property you were getting free has to be arranged. Caching, retries, failure signalling, rate limits and authentication are all outside the specification. None of that makes GraphQL worse. It makes the bill arrive later.
What is the difference, in each specification's own words?
The GraphQL specification introduces itself as the specification for a query language and execution engine for describing and performing the capabilities and requirements of data models for client-server applications. It then rules things out. GraphQL is not a programming language capable of arbitrary computation, and it does not mandate a particular programming language or storage system for the services that implement it. That is a narrower claim than most comparisons assume. It describes a language, a type system, a set of validation rules and an execution algorithm. It does not describe a network.
Its design principles are where the real difference lives. Under the heading for a client-specified response, the document draws the contrast itself: in the majority of client-server applications written without GraphQL, the service determines the shape of data returned from its various endpoints, whereas a GraphQL response contains exactly what a client asks for and no more. Two other principles matter to someone maintaining an app. Requests are hierarchical, so the request is shaped just like the data in its response. And the service is self-describing: the type system can be queried by the GraphQL language itself, through the __schema and __type meta-fields, which is why editors and code generators can learn your API without being told about it.
Validation is the other thing you get in the box. A GraphQL service does not just verify that a request is syntactically correct, the specification says, but also that it is unambiguous and mistake-free in the context of a given schema. That check can run at development time, which is the part that pays off: a query your build validated against the schema will not fail later because somebody misspelled a field.
REST has no equivalent document, and that is not a criticism. What it has instead is HTTP, whose request methods carry meaning that is written down and not negotiable. MDN describes a method as indicating the purpose of the request and what is expected if the request is successful, and classifies each one as safe, idempotent or cacheable. GET requests a representation of a resource and should not carry request content. POST submits an entity, often causing a change in state or side effects on the server. PUT replaces the current representations of the target resource. Those are not conventions somebody invented for your API. They are properties any cache, proxy or client library is entitled to rely on.
GraphQL specification, September 2025 edition, read 17 October 2026
Which way does your project push?
Tick what is true of the app you have. Nothing here is a verdict. It tells you which pressure you are under and why.
Nothing ticked. An app with one list screen and one detail screen is not being pulled in either direction, and the right answer is whatever the project already has.
Which of the two can your app actually cache?
This is the trade-off that matters on a phone, because a phone spends its life on a connection that is sometimes there. MDN's request methods reference carries a table of which methods are safe, idempotent and cacheable. GET, HEAD and QUERY are all three. PUT and DELETE are idempotent but not cacheable. POST and PATCH are marked cacheable only on conditions, and the footnote spells the conditions out: they are cacheable when responses explicitly include freshness information and a matching Content-Location header. The page was last modified on 23 September 2026.
A GraphQL client normally sends one POST to one path. So the whole apparatus above does nothing for it by default, and not because anybody decided GraphQL should be uncacheable. The specification simply does not reach that far. Searching the September 2025 edition for caching turns up two mentions, and neither is about HTTP. One is a note encouraging implementations to coerce an argument's default value once and cache the result. The other is the definition of the ID scalar, described as a unique identifier often used to refetch an object or as the key for a cache. That second one is the real story: GraphQL's caching answer lives in the client, keyed by object identity, which is how normalized client caches work.
That is a different kind of cache with different failure modes. An HTTP cache is shared and keyed by URL, so a popular read is served once from the edge for everybody. A client cache is private, keyed by object, and dies when the app is uninstalled, but it can answer a query assembled from objects fetched by three other screens. If your app mostly reads the same public data on cold start, the HTTP one is worth more. If it mostly reads one user's own records across many screens, the client one is.
One piece of this is newer than the usual argument. The method table also lists QUERY, which MDN describes as initiating a server-side query, asking the target resource to process the request content in a safe and idempotent manner and return the result. It is similar to GET but allows request content with defined semantics, and the table marks it safe, idempotent and cacheable. On paper that removes the reason a query with a body cannot be cached. Whether your server, and every cache between it and the handset, implements it is a separate question, and the method table is not where you find that out.
Worth knowing what your own framework contributes here, which is nothing. React Native's networking documentation, read on 17 October 2026, says React Native provides the Fetch API for your networking needs and that the XMLHttpRequest API is built in, which is why third-party clients work. It opens by describing a POST request to a REST API, never mentions GraphQL, and does not discuss caching anywhere on the page. Whatever caching your app has, it came from the platform HTTP stack or from a library somebody added.
MDN, HTTP request methods, read 17 October 2026, page last modified 23 September 2026
How does each one tell you that a request failed?
GraphQL sorts failures into two kinds and treats them differently. A request error, the specification says, is typically the fault of the requesting client. When one is raised the response must not contain a data entry at all, the errors entry must include the error, and execution should be halted. That is your malformed query, your unknown field, your variable of the wrong type. An execution error is the other kind, and the document is blunt about whose fault it usually is: an execution error is typically the fault of a GraphQL service. It happens at one position inside the response, nested execution halts there, and sibling execution carries on, producing partial result.
Partial is the word to sit with. Your app can receive a response that is half filled in, with the gaps as nulls and an errors list explaining them. Every error must carry a message written for a developer. It should carry locations, with a line and a column, and a path identifying the response position that failed. And nullability decides how far the damage spreads: when an error lands on a Non-Null position, it propagates to the parent position, and if every position up the chain is Non-Null the whole data entry becomes null. One wrong field can empty a screen, depending on how the schema was written, which is not obvious from reading your own client code.
Now the part that catches teams out. The specification never mentions status codes. Reading the September 2025 edition, the phrase does not appear once, and neither does REST. Because the document does not describe a transport, it has nothing to say about what the response code should be when a field blows up, so most servers return a success code with errors in the body. Any dashboard, uptime check or alert rule that counts non-success responses will therefore report a badly broken GraphQL API as completely healthy. With REST you get the opposite problem, which is the better problem: the status code is the answer, available before anything parses the body.
There is a second asymmetry worth noticing. HTTP tells you which calls are safe to repeat, so a client can retry a timed-out GET or PUT without thinking. GraphQL uses the word idempotent exactly once, and not about requests: it says field resolution other than top-level mutation fields must be side effect-free and idempotent so the executor may run fields in any order. That is a statement about your resolvers, not about your retries. If you auto-retry a dropped mutation, you are the one who has to know whether running it twice charges the customer twice. When one of these does go wrong in production, it shows up in the same place everything else does, so it is worth knowing your way around debugging a crash before you need to.
GraphQL specification, sections 6.4.4 and 7.1.6, read 17 October 2026
What happens when the API changes under an app you already shipped?
A website has one version live at a time. An app does not. People keep builds for years, some of them never update, and every one of those builds keeps calling your API with the request it was compiled against. So the real question about API style on mobile is not which is nicer to write. It is which one survives a server change made eighteen months after a build shipped.
GraphQL has more to say about this than people expect, and it is the opposite of the usual assumption that REST is the stable one. Under Type System Evolution, the specification observes that as a schema evolves by adding types and fields, a request that was previously valid could later become invalid, and defines the term: any change that can cause a previously valid request to become invalid is considered a breaking change. It then says services and schema maintainers are encouraged to avoid breaking changes. There is also a tool for the honest path. The built-in deprecation directive marks fields, arguments, input fields and enum values as deprecated, with a reason that defaults to the text No longer supported, and it may not be applied to a required argument or input field. Introspection can report deprecated parts of the schema when a client asks for them.
Compare that with the REST side and the asymmetry is striking. The MDN method reference, which is the first-party description of what HTTP methods mean, does not mention versioning or deprecation at all. That is not an omission. HTTP gives you methods and status codes, not a policy for changing what a resource returns. Putting a version in the path, or in a header, or running two endpoints side by side, is a convention your team picked. It usually works, and nothing will remind you when you break it, because there is nothing in the protocol to break.
There is one lever a React Native app has that a native app does not, and it is worth being precise about its limits. Your networking code is JavaScript, so it can be replaced without a new binary and without a store review, which is how over the air updates are usually used for exactly this sort of fix. What it does not do is reach a phone that is offline, or one whose owner opens the app once a quarter. Treat it as a way to shorten the tail, not to delete it. The safe rule on both styles is the same: add fields, never repurpose one, and keep the old shape answering until the logs say nobody is asking for it.
MDN, HTTP request methods, checked for any mention of versioning on 17 October 2026
The same questions, answered by each document
Read the last column first. Anything marked yours is a job the documents hand back to you no matter which style you pick, so it cannot be a reason to choose either.
| The question | GraphQL specification, September 2025 | HTTP, as the method reference documents it | Left to you either way |
|---|---|---|---|
| Who decides the shape of the response | The client. A response contains exactly what a client asks for and no more | The service, one endpoint at a time | No |
| Which transport you use | Not specified. Chosen by the implementing service | HTTP, with the meaning of each method written down | No |
| Whether a response may be cached | Not addressed anywhere in the specification | GET, HEAD and QUERY cacheable, POST and PATCH on conditions | No |
| Whether a failed call is safe to repeat | Not addressed for requests | Safe and idempotent are listed per method | No |
| How a failure reaches the app | An errors entry carrying message, locations and path | A status code, plus whatever the body says | No |
| How the API tells you it changed | A deprecation directive, and a defined term for a breaking change | Not mentioned in the method reference | No |
| Authentication, rate limits and paging | Yours | Yours | Yours |
Where this lands if a builder generated the project
Nothing above changes because an agent wrote the first version of the networking code. The useful thing is that it is code. Newly generates a real React Native and Expo project you own, with two-way GitHub sync, so the file that talks to your API is a file you can open, read and replace. It is paid from $25 a month with no free tier. If you want to know where the line sits between what the tool produced and what is yours to change, that is the subject of who owns AI generated code.
Practically, swapping styles is a JavaScript change. Neither a GraphQL client nor a fetch call needs anything from the native side, so you are not rebuilding to try the other one. That matters because the build path is where mobile time goes: Newly uploads iOS builds to TestFlight, publishes to Google Play internal testing and builds a standalone Android APK, and a change confined to the JavaScript layer does not force you back through all of it.
Two honest limits. First, the thing you cannot regenerate is the server at the other end, so if your API is someone else's, the choice was made for you and the only question left is how well your client handles it. Second, Newly does not submit the app to the App Store for you, because Apple requires the content owner to submit, which means you will be the one reading the release notes and the crash reports when a schema change goes wrong. Pick the style the person holding that pager can debug.
Questions people ask about GraphQL and REST on mobile
Decide it on caching and on failure handling, not on speed. REST is HTTP, and HTTP writes down which methods are safe, which are idempotent and which responses may be cached, so you inherit those answers. The GraphQL specification deliberately leaves transport to the implementer, so none of those answers come with it. In exchange GraphQL gives you a typed schema the client can query for itself, and a response that contains exactly what the client asked for and no more. If your app has a handful of screens pulling the same records, REST is less to own. If different screens need different slices of a large object graph, GraphQL earns its keep.
Describe the app, then decide what the screens actually need
Write down your screens and what each one has to load. If they all want the same records, REST is less to own. If each wants a different slice of one large graph, GraphQL earns the extra work. Either way the project is yours, and either way the caching and the retries are your call.
Start building