import { createHash } from "crypto";
import { readFileSync } from "fs";
export function sha256(query: string): string {
return createHash("sha256").update(query, "utf8").digest("hex");
}
type RawManifest = Record<string, string>;
export function loadManifest(path: string): ReadonlyMap<string, string> {
const raw = JSON.parse(readFileSync(path, "utf8")) as RawManifest;
const map = new Map<string, string>();
for (const [hash, query] of Object.entries(raw)) {
const actual = sha256(query);
if (actual !== hash) {
throw new Error(
`Manifest integrity error: entry ${hash} does not match its query (got ${actual})`
);
}
map.set(hash, query);
}
return Object.freeze(map);
}
import type { ApolloServerPlugin, GraphQLRequestContext } from "@apollo/server";
import { GraphQLError } from "graphql";
class PersistedQueryNotFoundError extends GraphQLError {
constructor(hash: string) {
super("PersistedQueryNotFound", {
extensions: { code: "PERSISTED_QUERY_NOT_FOUND", hash },
});
}
}
interface Options {
manifest: ReadonlyMap<string, string>;
allowArbitraryQueries: boolean;
}
export function persistedQueryPlugin(opts: Options): ApolloServerPlugin {
const { manifest, allowArbitraryQueries } = opts;
return {
async requestDidStart() {
return {
async didResolveSource(ctx: GraphQLRequestContext<any>) {
const req = ctx.request;
const apq = (req.extensions?.persistedQuery ?? {}) as {
sha256Hash?: string;
};
if (apq.sha256Hash) {
const query = manifest.get(apq.sha256Hash);
if (!query) {
throw new PersistedQueryNotFoundError(apq.sha256Hash);
}
req.query = query;
return;
}
if (req.query && !allowArbitraryQueries) {
throw new GraphQLError("Arbitrary queries are not permitted", {
extensions: { code: "PERSISTED_QUERY_REQUIRED" },
});
}
},
};
},
};
}
import express from "express";
import cors from "cors";
import { json } from "body-parser";
import { ApolloServer } from "@apollo/server";
import { expressMiddleware } from "@apollo/server/express4";
import { loadManifest } from "./query-manifest";
import { persistedQueryPlugin } from "./persistedQueryPlugin";
import { typeDefs, resolvers } from "./schema";
async function main() {
const isProd = process.env.NODE_ENV === "production";
const manifest = loadManifest(process.env.QUERY_MANIFEST ?? "./persisted-queries.json");
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: !isProd,
plugins: [
persistedQueryPlugin({
manifest,
allowArbitraryQueries: !isProd,
}),
],
});
await server.start();
const app = express();
app.use(
"/graphql",
cors(),
json({ limit: "32kb" }),
expressMiddleware(server, {
context: async ({ req }) => ({ token: req.headers.authorization }),
})
);
app.listen(4000, () => {
console.log(`Serving ${manifest.size} persisted queries on :4000`);
});
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
Persisted queries flip the usual GraphQL contract: instead of clients sending arbitrary query strings, they send only a SHA-256 hash that maps to a query the server already knows. This snippet implements the stricter allowlist variant, where any hash not present in a build-time manifest is rejected outright. That closes off two problems at once — it shrinks request payloads to a tiny hash, and it prevents attackers from crafting expensive or introspection-heavy queries, since only vetted operations can run.
The query-manifest file loads a JSON map produced during the client build (typically by a codegen tool that walks all .graphql documents). loadManifest reads that file once, verifies each entry's hash actually matches its query text via sha256, and returns a frozen Map. Verifying at load time catches a corrupted or tampered manifest early rather than mid-request. The sha256 helper is the single source of truth for how hashes are computed, so client and server stay in agreement.
The heart of the enforcement lives in persistedQueryPlugin, an Apollo Server plugin implementing the requestDidStart lifecycle. In didResolveSource it reads the APQ extension (extensions.persistedQuery.sha256Hash) that Apollo's link protocol sends. If a hash is present, it is looked up in the manifest and the corresponding query is injected into the request; a missing hash becomes a PersistedQueryNotFoundError. Crucially, if a request arrives with a raw query string but allowArbitraryQueries is false, the plugin throws — this is what makes it an allowlist rather than a cache. The standard APQ flow lets clients register unknown queries; the allowlist deliberately removes that escape hatch.
server wires it together with expressMiddleware, disabling introspection in production and layering the plugin ahead of execution. Note the trade-off: the allowlist requires the client and server manifests to be deployed in lockstep, so a rolling deploy needs both old and new hashes valid simultaneously. Keeping the manifest immutable and versioning it alongside releases avoids the classic pitfall where a freshly deployed client sends hashes the server has not yet learned.
Related snips
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: String!
GraphQL API with Spring Boot
Rails.application.configure do
config.after_initialize do
Bullet.enable = true
Bullet.alert = false
Bullet.bullet_logger = true
Bullet.console = true
N+1 query detection with Bullet gem
export type Settled<R> =
| { status: 'fulfilled'; value: R }
| { status: 'rejected'; reason: unknown };
export interface ConcurrencyOptions {
limit: number;
Simple concurrency limiter for batch operations
Share this code
Here's the card — post it anywhere.