GraphQL APIs represent every resource as a node with an id field. The format of that ID matters: it affects pagination, caching, global object identification, and the information you inadvertently expose to clients. UUID v7 as GraphQL node IDs gives time-ordered pagination, globally unique node IDs without coordination, and self-describing creation timestamps. This guide covers the GraphQL-specific patterns — custom UUID scalar definition, cursor pagination implementation, Relay global IDs, and framework integration using graphql-scalars and the uuid npm package.
Custom UUID scalar
Define a UUID scalar to enforce RFC 9562 format at the schema level:
scalar UUID
type Order {
id: UUID!
customerId: UUID!
total: Float!
createdAt: String!
}
type Query {
order(id: UUID!): Order
orders(after: UUID, first: Int): OrderConnection!
}
Implement the scalar with format validation using graphql-scalars:
import { UUIDResolver } from 'graphql-scalars';
const resolvers = {
UUID: UUIDResolver,
Query: {
order: async (_, { id }) => db.orders.findById(id),
}
};
graphql-scalars validates the 8-4-4-4-12 hexadecimal format, version digit (1–8), and variant bits (8, 9, a, or b) on all UUID inputs and returns a descriptive error for invalid values.
Cursor-based pagination with UUID v7
UUID v7 cursors are stable across inserts because new records always have larger IDs:
type OrderEdge {
node: Order!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
endCursor: String
}
type OrderConnection {
edges: [OrderEdge!]!
pageInfo: PageInfo!
}
Resolver implementation:
async orders(_, { after, first = 20 }) {
const cursor = after ? Buffer.from(after, 'base64').toString() : null;
const rows = await db.query(`
SELECT * FROM orders
WHERE ($1::uuid IS NULL OR id > $1::uuid)
ORDER BY id ASC
LIMIT $2
`, [cursor, first + 1]);
const hasNextPage = rows.length > first;
const edges = rows.slice(0, first).map(row => ({
node: row,
cursor: Buffer.from(row.id).toString('base64')
}));
return {
edges,
pageInfo: {
hasNextPage,
endCursor: edges.at(-1)?.cursor ?? null
}
};
}
The WHERE id > $cursor clause works because UUID v7 lexicographic order equals creation order — guaranteed by RFC 9562 §5.7. No ORDER BY created_at column needed.
Relay Global Object IDs
Relay requires node IDs to be globally unique across all types. Encode type + UUID v7:
function toGlobalId(type, id) {
return Buffer.from(`${type}:${id}`).toString('base64');
}
function fromGlobalId(globalId) {
const [type, id] = Buffer.from(globalId, 'base64').toString().split(':');
return { type, id };
}
// In resolver
const nodeId = toGlobalId('Order', order.id);
// e.g. "T3JkZXI6MDE5MjM2YTctYjRmMi03MDAwLTg..."
The UUID v7 within the global ID is globally unique per RFC 9562, so the TypeName:UUID combination is globally unique without any additional coordination. The Relay server specification documents the full Node interface and node(id: ID!) query pattern.
Code-first schema (TypeGraphQL / NestJS)
import { ObjectType, Field, ID } from 'type-graphql';
import { v7 as uuidv7 } from 'uuid';
@ObjectType()
class Order {
@Field(() => ID)
id: string = uuidv7(); // UUID v7 assigned at construction
@Field()
total: number;
}
The TypeGraphQL documentation covers @ObjectType, custom scalars, and resolver patterns. NestJS GraphQL module wraps TypeGraphQL and adds dependency injection. The @Field(() => ID) decorator maps to the built-in ID scalar; replace with a custom UUID scalar if you want format validation.
Spring Boot GraphQL (Java)
@Controller
public class OrderController {
@QueryMapping
public Order order(@Argument String id) {
return orderRepository.findById(UUID.fromString(id)).orElseThrow();
}
@MutationMapping
public Order createOrder(@Argument CreateOrderInput input) {
Order order = new Order();
order.setId(UuidCreator.getTimeOrderedEpoch()); // UUID v7
return orderRepository.save(order);
}
}
The Spring for GraphQL documentation covers @QueryMapping, @MutationMapping, and the RuntimeWiringConfigurer for custom scalars. Use the uuid-creator library for Java UUID v7 generation.
What UUID v7 discloses in a GraphQL API
UUID v7 embedded in a GraphQL id field discloses the creation timestamp to the millisecond. For resources where creation time is sensitive (user accounts, internal documents), use UUID v4. For resources where creation order is a useful feature (orders, events, messages), UUID v7 is appropriate. The UUID Security guide covers this trade-off in full.
Further reading
- UUID Security — timestamp exposure and when to use v4 instead
- UUID v4 vs UUID v7 — choosing between the two versions
- UUID in JavaScript —
crypto.randomUUID()and the uuid package
External references
- graphql-scalars UUID scalar
- Relay Cursor Connections specification
- Relay Global Object ID specification
- RFC 9562 — UUID standard
- uuid npm package
Frequently asked questions
Should I use the GraphQL ID type or a custom UUID scalar?
The built-in ID scalar is serialised as a string and imposes no format constraints — it accepts integers and UUIDs equally. A custom UUID scalar adds format validation (RFC 9562 structure check) and makes the schema self-documenting. The graphql-scalars library provides a production-ready UUID scalar that validates format on both input and output. Use the custom scalar when clients expect UUID v7 format and you want schema-level enforcement.
How do I use UUID v7 for cursor-based pagination in GraphQL?
UUID v7 cursors encode position and time in one value. Use the UUID v7 string (base64-encoded if preferred) as a cursor; fetch the next page with WHERE id > $cursor ORDER BY id ASC. Because UUID v7 values are lexicographically ordered by creation time, this gives stable, time-ordered pagination without a separate created_at cursor field. The Relay Cursor Connections specification defines the edges/node/cursor pattern that most GraphQL clients expect.
How does UUID v7 relate to the Relay Global Object ID specification?
The Relay Global Object ID specification requires each node to have a globally unique opaque id — typically a base64-encoded string of TypeName:dbId. Using UUID v7 as the dbId means the base64-decoded ID encodes both the type and creation time, enabling server-side routing without a separate lookup. UUID v7's global uniqueness (per RFC 9562) satisfies the Relay requirement that IDs are unique across all types.