Key takeaways
- The real difference between GraphQL clients is the cache: Apollo Client and Relay keep a normalized cache, urql caches whole documents by default, and graphql-request has no cache at all.
- Relay is built for React and expects a server that follows its conventions; Apollo Client and urql work across several frameworks.
- If you only call a GraphQL endpoint from a script or a server, a thin client such as graphql-request is usually enough.
- Whatever client your front end uses, a GraphQL API is load tested as plain HTTP POST requests, and its limits (query depth, cost) matter as much as its speed.
The best GraphQL client for JavaScript depends on what you need the client to remember. Apollo Client is the broadest choice for apps that need a normalized cache and state management, Relay suits large React apps willing to follow its conventions, urql is a modular option across React, Vue, Svelte and others, and graphql-request is the minimal choice for scripts and server code. AWS Amplify’s data client is a fifth option, but it only makes sense if your backend is Amplify.
Versions, licences and release dates below were read from the npm registry and each project’s own documentation on 2026-10-23.
Comparison of the 5 GraphQL clients
| Client | Current npm version | Licence | Frameworks | Cache | Best for |
|---|---|---|---|---|---|
| Apollo Client | @apollo/client 4.3.5 | MIT | React built in; Vue, Angular, Svelte and others through community integrations | Normalized | Apps that need caching and local state in one library |
| Relay | react-relay 21.0.1 | MIT | React | Normalized store | Large React apps with a Relay-style schema |
| urql | urql 5.0.4 (@urql/core 6.0.3) | MIT | React, Preact, Vue, Solid, Svelte | Document cache by default, normalized with Graphcache | Projects that want to swap behaviour through exchanges |
| graphql-request | 7.4.0 | MIT | None (framework agnostic) | None | Scripts, servers and small apps |
| AWS Amplify | aws-amplify 6.22.2 | Apache-2.0 | JavaScript (aws-amplify package) | Handled by the Amplify data client | Apps whose backend is an Amplify GraphQL API |
Which GraphQL client should I use?
Match the client to the job. Pick Apollo Client when several components share data and you want one cache. Pick Relay when you run a large React app and can shape the server to its rules. Pick urql when you want to start small and add features, such as a normalized cache, only when you need them. Pick graphql-request when there is no UI to keep in sync, for example a Node script, a build step or a server-to-server call. Pick Amplify only when the API behind it is an Amplify one.
Apollo Client
Apollo Client describes itself as “a comprehensive GraphQL state management library for JavaScript”. It has a normalized cache, so a query can be answered locally when the data is already there, and React support (hooks and Suspense) is part of the core package. Angular, Vue, Svelte, Solid, Ember and Meteor are covered by community integrations, and Apollo documents separate Swift (iOS) and Kotlin (Android) clients. The npm package is at 4.3.5 and the install is npm install @apollo/client graphql.
- Good at: one library for fetching, caching and local state, with browser devtools.
- Cost to know about: it is more machinery than a small project needs, and its cache has its own concepts (type policies, cache keys) to learn.
Relay
Relay is “a GraphQL client for React”, maintained by Meta. Each component declares the data it needs, Relay combines those declarations into queries, and a normalized store keeps components that read the same data in sync. Its documentation lists pagination, refetching, optimistic updates and rollbacks among the patterns it implements for you. The current npm package, react-relay, is 21.0.1.
- Good at: big React codebases where many components touch the same data.
- Cost to know about: the server has to follow Relay’s GraphQL server specification: objects refetchable through a
Nodeinterface with a rootnode(id: ID!)field, and lists exposed as connections withedges,cursorandpageInfo. It is not an add-on for a schema built without those conventions.
urql
urql is described in its README as “a highly customisable and versatile GraphQL client”. One package gets you a working client in React, Preact, Vue, Solid or Svelte. The default cache is a document cache, and a normalized cache is a separate package, @urql/exchange-graphcache. Behaviour is changed through exchanges, urql’s name for the pieces of its request pipeline. The urql package is at 5.0.4 and @urql/core at 6.0.3.
- Good at: starting small and adding behaviour later without changing client.
- Cost to know about: the range of tutorials and tooling is smaller than Apollo’s, and a document cache invalidates whole results, so mutations need care when the cache is not normalized.
graphql-request
graphql-request bills itself as the “most simple & lightweight GraphQL client”. It runs in Node and the browser, supports TypeScript including TypedDocumentNode, ships as a pure ESM package, and needs graphql installed beside it. Its README states that it has no built-in cache and no front-end framework integrations. The repository now lives under the graffle-js organisation, and the latest npm release (7.4.0) was published in December 2025. Check the repository’s activity before you adopt it for something long-lived.
import { gql, request } from 'graphql-request'
const document = gql`
{
company {
ceo
}
}
`
await request('https://api.spacex.land/graphql/', document)
- Good at: getting a query out of a script in a few lines.
- Cost to know about: anything beyond sending requests, such as caching, retries or state, is yours to build.
AWS Amplify
AWS Amplify is not a general GraphQL client. Its data client is generated from your Amplify data schema with generateClient<Schema>(), and you call typed operations such as client.models.Todo.create() and client.models.Todo.list(). For live data, observeQuery() returns an observable that delivers the current items as they change. The API behind it is built on AWS AppSync with a DynamoDB database (docs.amplify.aws, October 2026). The npm package aws-amplify is at 6.22.2 under the Apache-2.0 licence.
Read the Amplify Data documentation before choosing it for an API that is not an Amplify one: the page I read describes the Amplify-backed path only. Costs come from the AWS services behind it, so AWS Amplify’s own pricing pages are the place to check.
How do you load test a GraphQL API?
A GraphQL API is load tested as HTTP, because that is how requests arrive. Over HTTP, a request is a POST with a JSON body holding the query, optional variables and an optional operationName, and a Content-Type: application/json header (graphql.org, October 2026). In k6 that is one http.post call with a JSON.stringify-ed body. The client libraries above do not matter to the server, which only sees the requests they produce.
Here is a k6 script that sends a parameterised query with a different variable on each iteration, checks the HTTP status, and checks that the response carries no GraphQL errors. I ran it with k6 1.7.1 against a local mock endpoint to confirm it runs; point GRAPHQL_URL at your own staging API.
import http from 'k6/http';
import { check, sleep } from 'k6';
const ENDPOINT = __ENV.GRAPHQL_URL || 'http://localhost:4010/graphql';
const codes = ['US', 'DE', 'FR', 'JP', 'BR'];
const query = `
query Country($code: ID!) {
country(code: $code) {
name
}
}`;
export const options = {
vus: 5,
duration: '10s',
thresholds: {
http_req_failed: ['rate<0.01'],
http_req_duration: ['p(95)<800'],
},
};
export default function () {
const code = codes[Math.floor(Math.random() * codes.length)];
const res = http.post(
ENDPOINT,
JSON.stringify({ query, variables: { code }, operationName: 'Country' }),
{ headers: { 'Content-Type': 'application/json' } },
);
check(res, {
'status is 200': (r) => r.status === 200,
'no GraphQL errors': (r) => {
const body = r.json();
return body.data && !body.errors;
},
});
sleep(1);
}
What is different about load testing GraphQL?
Four things differ from a REST test:
- One URL, many shapes. Every request goes to the same endpoint, so a report grouped by URL lumps a cheap lookup and an expensive nested query together. Give each operation its own tag or name so its percentiles are visible. The post on why percentiles beat averages explains why the 95th percentile of a mixed list hides the slow ones.
- A 200 can still be a failure. The GraphQL-over-HTTP specification allows a
2xxresponse that carries anerrorsarray when part of the result could be produced, and servers using the olderapplication/jsonmedia type may return2xxeven for validation failures. Check the body, as the script above does, not only the status code. - Vary the variables. The same query with the same variables is often answered from a server or CDN cache, which flatters the numbers. Draw variables from a realistic set, as the script does.
- Persisted queries change the payload. With persisted queries a client sends a short identifier or hash instead of the full query text. Apollo’s documentation separates automatic persisted queries, where the first request carries the full string, from a registered list that rejects unknown operations (apollographql.com, October 2026). A load test has to send what your real clients send.
Should a GraphQL load test include depth and complexity limits?
Yes, test them. Clients choose their own fields, so request counts alone say little about load. The GraphQL project’s security guidance recommends limiting the depth of an operation, with a smaller limit for nested lists, estimating query cost from weights on types and fields, and rejecting requests above a budget (graphql.org/learn/security, October 2026). Add test cases that send a query one level over your depth limit and confirm the server rejects it quickly, and a heavy but allowed query to see how much work one request can cost. Run those against a staging system you own.
To run a script like this from many regions without maintaining your own load generators, LoadFocus runs k6 scripts you upload as a .js file from 25 AWS regions (loadfocus.com/k6-load-testing, October 2026), and its free API load test is a quicker way to try a single endpoint. For the basics of k6 itself, see k6 vs JMeter and running k6 at scale.
Frequently Asked Questions
Which GraphQL client is best for JavaScript?
There is no single best client. Apollo Client has the broadest feature set and a normalized cache, Relay is built for large React apps, urql is modular across several frameworks, and graphql-request is the smallest option for scripts and servers. Choose by how much client-side caching and state your app needs.
What is the easiest GraphQL client to start with?
For a script or a server, graphql-request: install it with graphql, call request() with a URL and a document. For a React app with a UI, urql’s single package gets a working client in a few lines, and you add a normalized cache later if you need one.
Can I use GraphQL with Node.js?
Yes. GraphQL is an HTTP-based query language, so any Node.js process can call a GraphQL API with fetch, or with a client library such as graphql-request or Apollo Client, and Node.js can also host a GraphQL server through libraries built on the graphql package.
Do I need a GraphQL client at all?
Not always. A GraphQL query is an HTTP POST with a JSON body, so plain fetch works for occasional calls. A client earns its place when you need a cache, request deduplication, or generated types.
Is GraphQL better than REST?
It depends on the API. GraphQL lets a client ask for exactly the fields it needs from one endpoint, which can cut round trips for screens that combine several resources. REST is simpler to cache at the HTTP layer and easier to rate limit by URL. Pick on the shape of your data and consumers.
How do I load test a GraphQL API?
Send the real queries as HTTP POST requests from a load testing tool such as k6 or JMeter, with varied variables, checks on the response body for GraphQL errors, and per-operation reporting. Include tests for your depth and complexity limits.
Sources
- Apollo Client documentation and README: apollographql.com/docs/react, github.com/apollographql/apollo-client (October 2026)
- Relay documentation and server specification: relay.dev/docs (October 2026)
- urql README: github.com/urql-graphql/urql (October 2026)
- graphql-request README: github.com/graffle-js/graffle, branch graphql-request (October 2026)
- Amplify Data: docs.amplify.aws/react/build-a-backend/data/set-up-data (October 2026)
- Package versions and licences: registry.npmjs.org, read 2026-10-23
- GraphQL over HTTP: graphql.org/learn/serving-over-http (October 2026); security guidance: graphql.org/learn/security (October 2026)
- Persisted queries: apollographql.com/docs/graphos/platform/security/persisted-queries (October 2026)
- k6
http.post: grafana.com/docs/k6/latest/javascript-api/k6-http/post (October 2026)