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.

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.