Skip to main content

GCA Best Practices

This guide collects production-ready recommendations for designing, securing, and operating GCA endpoints. They assume familiarity with the content type operations, view models, and the settings reference.

Schema design

Expose the minimum that serves the use case. Every entry type, reference, and field you expose is API surface you must support and secure. Start from the consumer's needs, not from your content model: add entry types one at a time, use fieldFilter to trim internal fields, and rely on the default-deny referenceTypeFilter so reference traversal is a deliberate decision.

Separate endpoints by audience. A public delivery endpoint, an ingestion endpoint, and an internal tooling endpoint have different security postures and schema shapes. Endpoints are cheap—create one per use case rather than one mega-endpoint with compromises.

Prefer view models for public delivery. Content models expose your storage shape; renaming a Java field is a breaking API change. View models give you a deliberate, stable contract and a place for server-side business logic. For strict delivery endpoints, disallowAllStateAccess() removes record state entirely.

Decide on group-by and type-specific entry fields early. Both allowGroupByQueries() (return types become unions) and includeTypeSpecificEntryFields() change the schema shape; flipping them later breaks existing queries.

Mind modification inlining. inlineModificationFieldsWhenOnlyOneTypeExists() reads nicely but turns "add a second modification" into a breaking change. Leave it off for schemas with a long life expectancy.

Performance

Bound your queries. Keep maximumQueryLimit realistic for your database, set maximumQueryTimeout, and configure a global timeout(...). Slow queries hold connections; timeouts convert pathological queries into fast errors.

Use query complexity budgets. Reference fields cost database fetches; embedded fields don't. Configure referenceFieldComplexity higher than embeddedFieldComplexity and add a static complexity manager so expensive queries are rejected before execution. See Security.

Count cheaply. When a consumer only needs a total, selecting only pageInfo { count } (no items) optimizes to a database count.

Paginate, don't crawl. Encourage consumers to use offset/limit with sorts on indexed fields. For "is there anything?" checks, count beats fetching a page of items.

Index deliberately. Only @Indexed fields are filterable and sortable. Index what consumers query—but every index has a write cost, so don't index speculatively.

Use persisted queries in production. Persisted queries cut request sizes and open the door to treating the registered set as an allowlist.

Security

Require API keys. The default GraphQLApiAccessOptionExplicit requires a key; keep it that way for anything non-public, and issue separate clients per consumer so usage is attributable and revocable.

Keep introspection off in production. The default introspectionQueryRule already disables GraphQL introspection in production—don't override it for public endpoints. Enable allowTypeSystemIntrospection() only on internal tooling endpoints.

Treat mutations as privileged. Expose mutableEntryClass and content actions only on endpoints whose audience should write. Wire toolUserSupplier/siteSupplier so writes are attributed and site-scoped, and prefer dedicated ingestion endpoints over adding mutations to delivery endpoints.

Leave verbose errors off. verboseClientErrors() is a development aid; production error detail belongs in your logs, not your API responses.

Audit the escape hatches. allowRawStateAccess() bypasses field filters wholesale; _raw view access serializes everything. Enable them only on locked-down internal endpoints.

Operating and evolving the schema

Watch the schema versions. Every schema load is captured as a GraphQLSchemaVersion record; the schema diff tool shows exactly what changed between versions. Review diffs when upgrading Brightspot or refactoring content types—schema changes you didn't intend are the ones that break consumers.

Additive changes are safe; removals and renames are not. Adding types and fields won't break clients. Removing fields, renaming types (including by introducing naming conflicts!), changing return types, and toggling shape-changing settings will. When a name conflict is possible, pin names with objectTypeTypeNameFunction.

Validate writes with dry runs. Migrations and integrations should run mutations with context: {dryRun: true} first—full validation, zero persistence.

Monitor with analytics and the profiler. Built-in analytics show per-client usage; the @debug directive profiles individual queries when investigating slowness.

Working with consumers

  • Point consumers at the GraphQL Explorer for self-serve schema documentation—then make sure your Javadoc is good, because it is the documentation.
  • Encourage fragments keyed to the Record interface for cross-type queries, and __typename whenever ... on is involved.
  • Publish your pagination and rate expectations (limits, complexity budget) so client developers design within them.

Next steps

  • Security — the full security feature reference
  • Common Best Practices — deployment and operations guidance that applies to all endpoint kinds

Was this page helpful?

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.