Quickstart: GraphQL
GraphQL only For a REST or other plain HTTP origin, follow the HTTP quickstart instead.
This takes you from a new account to a verified cache HIT in front of your own GraphQL API,
and then shows a mutation clearing that entry. Nothing here is throwaway: the service you create
and the config you write are the ones you keep. If your origin is not a GraphQL API, follow the
HTTP quickstart.
Before you start you need a GraphQL API that GraphPilot can connect to over the internet. It
does not have to be public. The examples use a products query and an updateProduct mutation;
substitute an operation from your own API.
-
Get an account and the CLI
Section titled “Get an account and the CLI”Sign up at cockpit.graphpilot.io and create an organization with New organization if you do not belong to one yet; the dialog asks you to accept the Terms of Service. Then install the
gpilotCLI and log in:Terminal window brew install graphpilot/tap/gpilotgpilot auth loginOther platforms: installing the CLI. Several organizations or identities: signing in.
-
Create the service
Section titled “Create the service”A service is one origin behind GraphPilot, with its own hostname, configuration and cache.
In the portal, choose New service on your organization’s page. The Create a new service dialog asks for a Service name (
Checkout API), an Origin URL (https://api.internal.example.com), a Subdomain (checkout-api) and an Environment. See creating a service for what each field accepts. Keep Deploy right away (no schema, default configuration) ticked.Select Create service. The next dialog shows the service’s signing key, once. Copy it into your secret manager, then select I have copied the key.
The service’s dashboard now shows Setting up checkout-api: GraphPilot deploys the service once as a plain pass-through, then looks for your GraphQL API behind it and shows what it found, such as the endpoint path and the number of types. Apply suggested configuration and schema turns GraphQL on with the schema it read. You can apply it, or select Keep as plain HTTP: step 5 deploys your own
schema.graphqleither way, because a schema read by introspection carries no caching directives. See set up a new service for every state of the setup.Then select the service in the CLI:
Terminal window gpilot service select checkout-apiTerminal window gpilot service create \--name "Checkout API" \--url https://api.internal.example.comgpilot service select checkout-apiThe subdomain is derived from the name unless you pass
--subdomain. The command prints the service’s signing key once, in the Signing key row; copy it into your secret manager then. GraphPilot never shows it again. See the signing key.The command asks
Deploy right away?; answer yes (the default). It then follows the same setup as the portal and, when it finds your GraphQL API with a schema, asks whether to apply the suggested configuration and schema. Either answer works for this quickstart, since step 5 deploys your ownschema.graphql.--deployskips the first question,--no-deployonly creates the service; seegpilot service create.Once the first deployment is live, your service is reachable at
https://checkout-api.k7m2qp.graphpilot.cloud(k7m2qpstands for your organization’s hostname label, which the portal shows). That takes one to two minutes, and for your organization’s first service a few minutes more, up to an hour, while its certificate is issued; the setup shows both. Until you deploy a schema, it is a plain pass-through: every request reaches your origin unchanged, and only responses your origin marks withCache-Controlare cached.GraphPilot forwards each request to your origin’s host with the path the client sent, so if your API answers on
/graphql, your clients callhttps://checkout-api.k7m2qp.graphpilot.cloud/graphql. How the origin URL is used (scheme, port, TLS) is on origin. -
Put your schema next to a config
Section titled “Put your schema next to a config”In the directory you will deploy from, you need two files.
schema.graphqlis your API’s schema, exactly as your server exposes it. GraphPilot reads it for your types, the key fields it derives surrogate keys from, and any caching directives. If you have no schema file, read one from your running API withgpilot schema pull, then add the directives from step 4 to it: introspection returns your types but no applied directives such as@cacheControl.gpilot.tomlis the service configuration. Every setting has a default, so an empty file is valid. GraphPilot treats requests to/graphqlas GraphQL. If your API answers on a different path, say so: until you do, a GraphQL operation sent to that path is refused with404andNOT_A_GRAPHQL_PATH, because the GraphQL checks run only on the paths listed here:gpilot.toml [request]graphql_paths = ["/api/graphql"] -
Make something cacheable
Section titled “Make something cacheable”GraphPilot does not decide on its own that a response may be reused. Your origin or your schema has to say so. Pick either; you can use both later.
Send a
Cache-Controlheader on the responses that may be cached:Cache-Control: s-maxage=60A header from your origin decides before your schema is consulted. How the two combine, and why
s-maxagerather thanmax-age, is on when a response is cached.Annotate the schema, the way Apollo’s cache control plugin does. Declare the directive and use it:
schema.graphql directive @cacheControl(maxAge: Intscope: CacheControlScopeinheritMaxAge: Boolean) on OBJECT | INTERFACE | UNION | FIELD_DEFINITIONdirective @vary(names: [String!]!) on ENUM_VALUEenum CacheControlScope {PUBLICPRIVATEORGANIZATION @vary(names: ["x-org"])}type Product {id: ID!name: String!}type Query {products: [Product!]! @cacheControl(maxAge: 60)}productsis the same for everyone, so it keeps the default scope,PUBLIC.ORGANIZATIONis a named scope, one entry per organization, for data that differs per customer. It needs a[[cache.policy.vary]]entry namedx-orgbefore you use it; see named scopes.PRIVATEis for what really differs per person.This path decides when your origin sends no
Cache-Controlheader, or one that states neither storage nor freshness. See lifetime from the schema. -
Deploy
Section titled “Deploy”Terminal window gpilot deployThis picks up
./schema.graphqland./gpilot.toml, shows what changed since the last deployment, and asks before uploading anything. It then waits for the deployment to go live, printing each step and any findings the build reported. A failed build never goes live. Flags for CI, other paths and not waiting are in the command reference. -
Verify the cache
Section titled “Verify the cache”Send the same query twice to your service hostname:
Terminal window export GRAPHPILOT_HOST=checkout-api.k7m2qp.graphpilot.cloudcurl -sS -D- -o/dev/null "https://$GRAPHPILOT_HOST/graphql" \-H 'content-type: application/json' \-d '{"query":"{ products { id name } }"}' | grep -i '^gp-'Use your API’s GraphQL path in place of
/graphqlif you setrequest.graphql_pathsin step 3.The first response is a
MISS: it came from your origin and was stored. The second is aHIT:gp-cache: HITgp-cache-hits: 1gp-cache-age: 4gp-cache-max-age: 60gp-cache-remaining: 56If you see
PASS, read the next section. -
See a write clear the cache
Section titled “See a write clear the cache”Send a mutation through GraphPilot that changes one of the products in that list and returns it:
Terminal window curl -sS -D- -o/dev/null "https://$GRAPHPILOT_HOST/graphql" \-H 'content-type: application/json' \-d '{"query":"mutation { updateProduct(id: \"1\", name: \"Blue shirt\") { id name } }"}'Repeat the query from step 6. The first response is a
MISSagain, with the new name from your origin, and the one after it is aHIT.Nothing was configured for this. The cached
productsresponse was tagged with a surrogate key for each product it contained, such asProduct:id:1. The mutation’s response contained product 1, so GraphPilot purged every cached response taggedProduct:id:1. A mutation purges only what it returns: see automatic invalidation for what is and is not purged.
If you see PASS
Section titled “If you see PASS”gp-cache: PASS means the response was not stored, and gp-cache-reason names the rule that
refused. These three cause most first-deployment misses:
- Your origin sets a cookie (
CACHE_SKIPPED_SET_COOKIE). A response that sets a session cookie is never stored. Frameworks that touch the session on every request do this by default. See the refusals. - Apollo Server sent
no-store(CACHE_SKIPPED_NO_STORE). It does so for every response its hints do not make cacheable. See Apollo Server. - Your hosting platform added
Cache-Control: private. Some platforms, such as Google Cloud Run with session affinity, add it to every response that has noCache-Controlof its own. GraphPilot then stores the response per caller, or not at all. See when your hosting platform addsCache-Control: private.
Every other reason is in why is nothing cached?, and a second
MISS where you expected a HIT in
troubleshooting.
Point your clients at it
Section titled “Point your clients at it”Change your client’s endpoint to the service hostname, keeping the path your API uses. Nothing else changes: GraphPilot speaks the same GraphQL over the same transports, and what reaches your origin is an ordinary GraphQL request.
import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client";
const client = new ApolloClient({ link: new HttpLink({ uri: "https://checkout-api.k7m2qp.graphpilot.cloud/graphql" }), cache: new InMemoryCache(),});import { Client, cacheExchange, fetchExchange } from "urql";
const client = new Client({ url: "https://checkout-api.k7m2qp.graphpilot.cloud/graphql", exchanges: [cacheExchange, fetchExchange],});import { Environment, Network, RecordSource, Store } from "relay-runtime";
const network = Network.create(async (operation, variables) => { const response = await fetch("https://checkout-api.k7m2qp.graphpilot.cloud/graphql", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ query: operation.text, variables }), }); return response.json();});
const environment = new Environment({ network, store: new Store(new RecordSource()) });const response = await fetch("https://checkout-api.k7m2qp.graphpilot.cloud/graphql", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ query: "{ products { id name } }" }),});const { data } = await response.json();Before you consider this finished, verify the request signature at your origin, so it only trusts traffic that came through GraphPilot, and keep your origin’s clock in sync, because the signature is time-bound. Both are on the go-live checklist.
Where to go next
Section titled “Where to go next”| You want to | Read |
|---|---|
| Understand exactly what gets cached and what does not | When a response is cached |
| Decide who shares a cached answer | Who shares an entry |
| Know what a mutation purges, and what it does not | Automatic invalidation |
| Clear something by hand | The purge API |
| Stop clients sending arbitrary queries | Persisted queries |
| Deploy from a pipeline | Deploy from CI |
| See every setting | Configuration reference |
| Serve it on your own hostname | Custom domains |