Schema design, resolvers, subscriptions, and GraphQL optimization
# GraphQL API Design Best Practices for Google Antigravity
Design scalable GraphQL APIs in your Google Antigravity projects following industry best practices. This guide covers schema design, resolvers, performance optimization, and security patterns.
## Schema Design Fundamentals
Create well-structured GraphQL schemas:
```graphql
# src/graphql/schema.graphql
# Use descriptive types with documentation
"""
Represents a user in the system.
Users can create posts, comments, and interact with other users.
"""
type User {
id: ID!
email: String!
username: String!
displayName: String
avatar: String
bio: String
createdAt: DateTime!
updatedAt: DateTime!
# Connections for pagination
posts(first: Int, after: String, orderBy: PostOrderBy): PostConnection!
followers(first: Int, after: String): UserConnection!
following(first: Int, after: String): UserConnection!
# Computed fields
postsCount: Int!
followersCount: Int!
followingCount: Int!
isFollowedByMe: Boolean!
}
"""
Relay-style connection for pagination
"""
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type UserEdge {
node: User!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
# Input types for mutations
input CreatePostInput {
title: String!
content: String!
tags: [String!]
published: Boolean = false
}
input UpdatePostInput {
title: String
content: String
tags: [String!]
published: Boolean
}
# Queries organized by domain
type Query {
# User queries
me: User
user(id: ID, username: String): User
users(first: Int, after: String, search: String): UserConnection!
# Post queries
post(id: ID!, slug: String): Post
posts(
first: Int
after: String
filter: PostFilter
orderBy: PostOrderBy
): PostConnection!
# Feed queries
feed(first: Int, after: String): PostConnection!
trending(first: Int, period: TrendingPeriod): [Post!]!
}
# Mutations with clear naming conventions
type Mutation {
# Auth mutations
login(email: String!, password: String!): AuthPayload!
signup(input: SignupInput!): AuthPayload!
logout: Boolean!
# User mutations
updateProfile(input: UpdateProfileInput!): User!
followUser(userId: ID!): User!
unfollowUser(userId: ID!): User!
# Post mutations
createPost(input: CreatePostInput!): Post!
updatePost(id: ID!, input: UpdatePostInput!): Post!
deletePost(id: ID!): Boolean!
publishPost(id: ID!): Post!
}
# Subscriptions for real-time updates
type Subscription {
postCreated: Post!
postUpdated(id: ID!): Post!
newNotification: Notification!
}
```
## Resolver Implementation
Build efficient resolvers with DataLoader:
```typescript
// src/graphql/resolvers/user.ts
import DataLoader from "dataloader";
import { db } from "@/lib/database";
import { GraphQLContext } from "../context";
// Create DataLoaders for batch loading
export function createUserLoaders() {
return {
userById: new DataLoader<string, User | null>(async (ids) => {
const users = await db.user.findMany({
where: { id: { in: [...ids] } },
});
const userMap = new Map(users.map((u) => [u.id, u]));
return ids.map((id) => userMap.get(id) || null);
}),
userPostsCount: new DataLoader<string, number>(async (userIds) => {
const counts = await db.post.groupBy({
by: ["authorId"],
where: { authorId: { in: [...userIds] } },
_count: { id: true },
});
const countMap = new Map(counts.map((c) => [c.authorId, c._count.id]));
return userIds.map((id) => countMap.get(id) || 0);
}),
userFollowersCount: new DataLoader<string, number>(async (userIds) => {
const counts = await db.follow.groupBy({
by: ["followingId"],
where: { followingId: { in: [...userIds] } },
_count: { id: true },
});
const countMap = new Map(counts.map((c) => [c.followingId, c._count.id]));
return userIds.map((id) => countMap.get(id) || 0);
}),
};
}
export const userResolvers = {
Query: {
me: async (_: unknown, __: unknown, ctx: GraphQLContext) => {
if (!ctx.user) return null;
return ctx.loaders.userById.load(ctx.user.id);
},
user: async (_: unknown, args: { id?: string; username?: string }, ctx: GraphQLContext) => {
if (args.id) {
return ctx.loaders.userById.load(args.id);
}
if (args.username) {
return db.user.findUnique({ where: { username: args.username } });
}
return null;
},
users: async (_: unknown, args: PaginationArgs & { search?: string }, ctx: GraphQLContext) => {
const { first = 20, after, search } = args;
const where = search
? {
OR: [
{ username: { contains: search, mode: "insensitive" } },
{ displayName: { contains: search, mode: "insensitive" } },
],
}
: undefined;
return paginateResults({
model: "user",
where,
first,
after,
orderBy: { createdAt: "desc" },
});
},
},
User: {
posts: async (parent: User, args: PaginationArgs, ctx: GraphQLContext) => {
return paginateResults({
model: "post",
where: { authorId: parent.id, published: true },
first: args.first || 10,
after: args.after,
orderBy: { createdAt: "desc" },
});
},
postsCount: (parent: User, _: unknown, ctx: GraphQLContext) => {
return ctx.loaders.userPostsCount.load(parent.id);
},
followersCount: (parent: User, _: unknown, ctx: GraphQLContext) => {
return ctx.loaders.userFollowersCount.load(parent.id);
},
isFollowedByMe: async (parent: User, _: unknown, ctx: GraphQLContext) => {
if (!ctx.user) return false;
const follow = await db.follow.findUnique({
where: {
followerId_followingId: {
followerId: ctx.user.id,
followingId: parent.id,
},
},
});
return !!follow;
},
},
Mutation: {
updateProfile: async (_: unknown, { input }: { input: UpdateProfileInput }, ctx: GraphQLContext) => {
if (!ctx.user) {
throw new AuthenticationError("Must be logged in");
}
return db.user.update({
where: { id: ctx.user.id },
data: input,
});
},
followUser: async (_: unknown, { userId }: { userId: string }, ctx: GraphQLContext) => {
if (!ctx.user) {
throw new AuthenticationError("Must be logged in");
}
if (userId === ctx.user.id) {
throw new UserInputError("Cannot follow yourself");
}
await db.follow.create({
data: {
followerId: ctx.user.id,
followingId: userId,
},
});
return ctx.loaders.userById.load(userId);
},
},
};
```
## Pagination Helper
Implement Relay-style cursor pagination:
```typescript
// src/graphql/utils/pagination.ts
interface PaginateOptions {
model: string;
where?: Record<string, unknown>;
first: number;
after?: string;
orderBy?: Record<string, "asc" | "desc">;
}
export async function paginateResults<T>(options: PaginateOptions) {
const { model, where, first, after, orderBy = { id: "asc" } } = options;
const take = first + 1; // Fetch one extra to check hasNextPage
const cursor = after
? { id: Buffer.from(after, "base64").toString() }
: undefined;
const items = await (db as any)[model].findMany({
where,
take,
skip: cursor ? 1 : 0,
cursor,
orderBy,
});
const hasNextPage = items.length > first;
const edges = items.slice(0, first).map((item: T & { id: string }) => ({
node: item,
cursor: Buffer.from(item.id).toString("base64"),
}));
return {
edges,
pageInfo: {
hasNextPage,
hasPreviousPage: !!after,
startCursor: edges[0]?.cursor,
endCursor: edges[edges.length - 1]?.cursor,
},
totalCount: await (db as any)[model].count({ where }),
};
}
```
## Query Complexity Analysis
Prevent expensive queries:
```typescript
// src/graphql/plugins/complexity.ts
import { getComplexity, simpleEstimator, fieldExtensionsEstimator } from "graphql-query-complexity";
const MAX_COMPLEXITY = 1000;
export const complexityPlugin = {
requestDidStart: () => ({
didResolveOperation({ request, document, schema }: any) {
const complexity = getComplexity({
schema,
query: document,
variables: request.variables,
estimators: [
fieldExtensionsEstimator(),
simpleEstimator({ defaultComplexity: 1 }),
],
});
if (complexity > MAX_COMPLEXITY) {
throw new Error(
`Query complexity ${complexity} exceeds maximum allowed ${MAX_COMPLEXITY}`
);
}
console.log(`Query complexity: ${complexity}`);
},
}),
};
```
Google Antigravity generates well-designed GraphQL APIs with proper schema structure, efficient resolvers, and built-in performance safeguards.This GraphQL prompt is ideal for developers working on:
By using this prompt, you can save hours of manual coding and ensure best practices are followed from the start. It's particularly valuable for teams looking to maintain consistency across their graphql implementations.
Yes! All prompts on Antigravity AI Directory are free to use for both personal and commercial projects. No attribution required, though it's always appreciated.
This prompt works excellently with Claude, ChatGPT, Cursor, GitHub Copilot, and other modern AI coding assistants. For best results, use models with large context windows.
You can modify the prompt by adding specific requirements, constraints, or preferences. For GraphQL projects, consider mentioning your framework version, coding style, and any specific libraries you're using.