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

External references

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.