GraphQL’s type system is flexible about identifier formats — the built-in ID scalar is an opaque string. Layering UUID validation and adopting UUID v7 for time-sortable connections unlocks better pagination, optimistic UI, and consistent cross-service identity.
GraphQL ID Scalar and UUID
The GraphQL spec defines ID as a unique identifier serialized as a string. It does not mandate UUID format — any string is technically valid. In practice, most APIs use UUIDs because they are globally unique and can be generated client-side.
Schema with plain ID scalar:
type Order {
id: ID!
status: String!
createdAt: String!
}
type Query {
order(id: ID!): Order
}
The problem: ID accepts "123", "not-a-uuid", and "". No validation happens at the schema layer.
Custom UUID Scalar
JavaScript (graphql-scalars):
npm install graphql-scalars
import { UUIDResolver } from 'graphql-scalars';
const typeDefs = `
scalar UUID
type Order {
id: UUID!
status: String!
}
type Mutation {
createOrder(id: UUID): Order!
}
`;
const resolvers = {
UUID: UUIDResolver,
Mutation: {
createOrder: (_, { id }) => {
// id is guaranteed to be a valid UUID string
},
},
};
UUIDResolver validates that any client-supplied UUID matches the RFC 9562 format and rejects malformed values with a descriptive error before the resolver runs.
UUID v7 for Cursor-Based Pagination
Cursor-based pagination with UUID v7 IDs is cleaner than offset pagination because UUID v7 values sort chronologically:
type OrderConnection {
edges: [OrderEdge!]!
pageInfo: PageInfo!
}
type OrderEdge {
node: Order!
cursor: String! # Base64-encoded UUID v7
}
type Query {
orders(first: Int, after: String): OrderConnection!
}
Resolver:
const resolvers = {
Query: {
orders: async (_, { first = 10, after }) => {
const afterId = after ? Buffer.from(after, 'base64').toString() : null;
const rows = await db.query(
`SELECT * FROM orders
WHERE id > $1 OR $1 IS NULL
ORDER BY id
LIMIT $2`,
[afterId, first + 1],
);
// ...build edges and pageInfo
},
},
};
WHERE id > $1 ORDER BY id uses the UUID v7 index efficiently — PostgreSQL performs a range scan on the sorted primary key index, not a full table scan. This is not possible with UUID v4 because v4 values have no meaningful sort order.
Client-Side ID Generation for Optimistic Mutations
With UUID v7, the client can generate the ID before the server creates the record:
import { v7 as uuidv7 } from 'uuid';
const CREATE_ORDER = gql`
mutation CreateOrder($id: UUID!, $total: Int!) {
createOrder(id: $id, total: $total) {
id
status
}
}
`;
// Apollo Client optimistic response
const id = uuidv7();
client.mutate({
mutation: CREATE_ORDER,
variables: { id, total: 9900 },
optimisticResponse: {
createOrder: { __typename: 'Order', id, status: 'pending' },
},
});
The optimistic UI updates immediately with the client-generated UUID v7 ID. If the server mutation succeeds, it confirms the same ID. If it fails, the optimistic cache entry is rolled back.
Schema Design — Exposing UUID Version
For APIs consumed by multiple clients, document which UUID version the API expects and generates:
"""
Globally unique resource identifier. Format: RFC 9562 UUID v7
(time-ordered, 36-character hyphenated hex string).
Client-generated IDs are accepted in mutations.
"""
scalar UUID
This is especially useful for client teams — knowing the server uses v7 tells them they can sort resources by ID to get creation order.
Related Resources
- graphql-scalars UUID scalar — ready-made UUID validation for JavaScript GraphQL servers
- Relay mutations documentation — client-side ID generation patterns
- GraphQL cursor-based pagination specification
- RFC 9562 — UUID v7 specification
Frequently asked questions
Should I use the GraphQL ID scalar or a custom UUID scalar?
The built-in ID scalar accepts any string — it does not validate UUID format. For APIs where clients pass UUIDs in mutations, define a custom UUID scalar that validates the 8-4-4-4-12 format. The graphql-scalars package provides a ready-made UUIDResolver for JavaScript/TypeScript GraphQL servers.
Do UUIDs hurt database index performance?
UUID v4 does — its randomness causes every insert to land at a different leaf page, leading to page splits and poor cache locality. UUID v7 embeds a millisecond timestamp so consecutive inserts cluster together, behaving like an auto-increment integer for B-tree purposes.
Should I use v4 or v7?
Use v7 for database primary keys (time-sortable, index-friendly) and v4 for anything where creation order could leak information, like tokens or share links.
Should I use the GraphQL ID scalar or a custom UUID scalar?
The built-in ID scalar serializes as a string and accepts any opaque value — it does not validate UUID format. For APIs where clients pass IDs in mutations, define a custom UUID scalar that validates the 8-4-4-4-12 format and rejects malformed input. Libraries like graphql-scalars provide a ready-made UUID scalar for JavaScript/TypeScript.
Can GraphQL clients generate UUID v7 for optimistic mutations?
Yes. UUID v7 is safe for client-generated IDs in optimistic mutations — clients generate the UUID before the server confirms the record. Use import { v7 as uuidv7 } from 'uuid' in the client. Because v7 embeds a timestamp, the server can detect IDs generated far in the past or future and reject them if needed. For sensitive resources, prefer server-side ID generation to prevent ID prediction. See Relay's mutation documentation for client-side ID patterns.