Building REST & GraphQL APIs with Google Antigravity: Complete Backend Development Guide | Antigravity AI Directory | Google Antigravity Directory
Building REST & GraphQL APIs with Google Antigravi... Tutorials Building REST & GraphQL APIs with Google Antigravity: Complete Backend Development Guide Building REST & GraphQL APIs with Google Antigravity: Complete Backend Development Guide
API development is where Google Antigravity truly shines. Gemini 3 can generate complete API endpoints, handle authentication, create validation schemas, and even write documentation. This guide covers building production-grade APIs.
API Architecture Options
When to Use Each
Type Best For Complexity REST Simple CRUD, public APIs Low GraphQL Complex data relationships Medium tRPC Type-safe full-stack apps Low gRPC Microservices, high performance High
REST API with Next.js
Basic API Route
You: "Create a REST API for managing blog posts with CRUD operations"
Gemini 3 generates:
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
import { z } from 'zod';
// Validation schemas
const createPostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(1),
published: z.boolean().default(false),
});
const querySchema = z.object({
page: z.coerce.number().min(1).default(1),
limit: z.coerce.number().min(1).max(100).default(10),
published: z.enum(['true', 'false']).optional(),
});
// GET /api/posts - List posts with pagination
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const query = querySchema.parse(Object.fromEntries(searchParams));
const supabase = createClient();
let queryBuilder = supabase
.from('posts')
.select('*, author:profiles(username, avatar_url)', { count: 'exact' });
if (query.published !== undefined) {
queryBuilder = queryBuilder.eq('published', query.published === 'true');
}
const { data, error, count } = await queryBuilder
.order('created_at', { ascending: false })
.range((query.page - 1) * query.limit, query.page * query.limit - 1);
if (error) throw error;
return NextResponse.json({
data,
pagination: {
page: query.page,
limit: query.limit,
total: count ?? 0,
totalPages: Math.ceil((count ?? 0) / query.limit),
},
});
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Validation failed', details: error.errors },
{ status: 400 }
);
}
console.error('GET /api/posts error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
// POST /api/posts - Create new post
export async function POST(request: NextRequest) {
try {
const supabase = createClient();
// Check authentication
const { data: { user }, error: authError } = await supabase.auth.getUser();
if (authError || !user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// Validate request body
const body = await request.json();
const validatedData = createPostSchema.parse(body);
// Create post
const { data, error } = await supabase
.from('posts')
.insert({
...validatedData,
author_id: user.id,
slug: validatedData.title.toLowerCase().replace(/\s+/g, '-'),
})
.select()
.single();
if (error) throw error;
return NextResponse.json({ data }, { status: 201 });
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Validation failed', details: error.errors },
{ status: 400 }
);
}
console.error('POST /api/posts error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
Related Posts Refactoring Legacy Code with Google Antigravity: AI-Powered Modernization Strategies Transform legacy codebases using Google Antigravity and Gemini 3. Learn strategies for modernizing old code, migrating frameworks, updating patterns, and safely refactoring with AI assistance.
Google Antigravity Workflow Automation: Custom Tasks, Hooks & AI-Powered Pipelines Automate your development workflow in Google Antigravity. Learn to create custom tasks, build automation hooks, configure AI pipelines, and boost productivity with intelligent automation.
Google Antigravity Artifacts Explained: Understanding AI-Generated Code Previews Deep dive into Google Antigravity Artifacts - the AI-generated code preview system. Learn how artifacts work, when to use them, customization options, and best practices for effective AI collaboration.
Dynamic Route for Single Resource // app/api/posts/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
import { z } from 'zod';
const updatePostSchema = z.object({
title: z.string().min(1).max(200).optional(),
content: z.string().min(1).optional(),
published: z.boolean().optional(),
});
interface Params {
params: { id: string };
}
// GET /api/posts/:id
export async function GET(request: NextRequest, { params }: Params) {
try {
const supabase = createClient();
const { data, error } = await supabase
.from('posts')
.select('*, author:profiles(username, avatar_url)')
.eq('id', params.id)
.single();
if (error) {
if (error.code === 'PGRST116') {
return NextResponse.json({ error: 'Post not found' }, { status: 404 });
}
throw error;
}
return NextResponse.json({ data });
} catch (error) {
console.error('GET /api/posts/[id] error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
// PATCH /api/posts/:id
export async function PATCH(request: NextRequest, { params }: Params) {
try {
const supabase = createClient();
// Check authentication
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// Check ownership
const { data: existingPost } = await supabase
.from('posts')
.select('author_id')
.eq('id', params.id)
.single();
if (!existingPost) {
return NextResponse.json({ error: 'Post not found' }, { status: 404 });
}
if (existingPost.author_id !== user.id) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
// Validate and update
const body = await request.json();
const validatedData = updatePostSchema.parse(body);
const { data, error } = await supabase
.from('posts')
.update({ ...validatedData, updated_at: new Date().toISOString() })
.eq('id', params.id)
.select()
.single();
if (error) throw error;
return NextResponse.json({ data });
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Validation failed', details: error.errors },
{ status: 400 }
);
}
console.error('PATCH /api/posts/[id] error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
// DELETE /api/posts/:id
export async function DELETE(request: NextRequest, { params }: Params) {
try {
const supabase = createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// Check ownership before delete
const { data: existingPost } = await supabase
.from('posts')
.select('author_id')
.eq('id', params.id)
.single();
if (!existingPost) {
return NextResponse.json({ error: 'Post not found' }, { status: 404 });
}
if (existingPost.author_id !== user.id) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const { error } = await supabase
.from('posts')
.delete()
.eq('id', params.id);
if (error) throw error;
return new NextResponse(null, { status: 204 });
} catch (error) {
console.error('DELETE /api/posts/[id] error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
GraphQL API
Setting Up GraphQL npm install @apollo/server graphql @as-integrations/next
Schema Definition You: "Create a GraphQL schema for a blog with posts, comments, and users"
// lib/graphql/schema.ts
import { gql } from 'graphql-tag';
export const typeDefs = gql`
scalar DateTime
type User {
id: ID!
email: String!
username: String!
avatar_url: String
posts: [Post!]!
comments: [Comment!]!
created_at: DateTime!
}
type Post {
id: ID!
title: String!
slug: String!
content: String!
excerpt: String
published: Boolean!
author: User!
comments: [Comment!]!
created_at: DateTime!
updated_at: DateTime!
}
type Comment {
id: ID!
content: String!
author: User!
post: Post!
created_at: DateTime!
}
type PostConnection {
nodes: [Post!]!
pageInfo: PageInfo!
totalCount: Int!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
input CreatePostInput {
title: String!
content: String!
published: Boolean = false
}
input UpdatePostInput {
title: String
content: String
published: Boolean
}
input CreateCommentInput {
postId: ID!
content: String!
}
type Query {
# Posts
posts(
first: Int = 10
after: String
published: Boolean
): PostConnection!
post(id: ID, slug: String): Post
# Users
me: User
user(id: ID!): User
}
type Mutation {
# Posts
createPost(input: CreatePostInput!): Post!
updatePost(id: ID!, input: UpdatePostInput!): Post!
deletePost(id: ID!): Boolean!
# Comments
createComment(input: CreateCommentInput!): Comment!
deleteComment(id: ID!): Boolean!
}
`;
GraphQL Resolvers // lib/graphql/resolvers.ts
import { createClient } from '@/lib/supabase/server';
import { GraphQLError } from 'graphql';
export const resolvers = {
Query: {
posts: async (_: unknown, args: { first: number; after?: string; published?: boolean }) => {
const supabase = createClient();
let query = supabase
.from('posts')
.select('*', { count: 'exact' });
if (args.published !== undefined) {
query = query.eq('published', args.published);
}
const { data, error, count } = await query
.order('created_at', { ascending: false })
.limit(args.first);
if (error) throw new GraphQLError(error.message);
return {
nodes: data,
totalCount: count ?? 0,
pageInfo: {
hasNextPage: (count ?? 0) > args.first,
hasPreviousPage: !!args.after,
},
};
},
post: async (_: unknown, args: { id?: string; slug?: string }) => {
const supabase = createClient();
let query = supabase.from('posts').select('*');
if (args.id) {
query = query.eq('id', args.id);
} else if (args.slug) {
query = query.eq('slug', args.slug);
} else {
throw new GraphQLError('Either id or slug must be provided');
}
const { data, error } = await query.single();
if (error) return null;
return data;
},
me: async (_: unknown, __: unknown, context: { userId?: string }) => {
if (!context.userId) return null;
const supabase = createClient();
const { data } = await supabase
.from('profiles')
.select('*')
.eq('id', context.userId)
.single();
return data;
},
},
Mutation: {
createPost: async (
_: unknown,
args: { input: { title: string; content: string; published?: boolean } },
context: { userId?: string }
) => {
if (!context.userId) {
throw new GraphQLError('Unauthorized', {
extensions: { code: 'UNAUTHORIZED' },
});
}
const supabase = createClient();
const { data, error } = await supabase
.from('posts')
.insert({
...args.input,
author_id: context.userId,
slug: args.input.title.toLowerCase().replace(/\s+/g, '-'),
})
.select()
.single();
if (error) throw new GraphQLError(error.message);
return data;
},
updatePost: async (
_: unknown,
args: { id: string; input: { title?: string; content?: string; published?: boolean } },
context: { userId?: string }
) => {
if (!context.userId) {
throw new GraphQLError('Unauthorized');
}
const supabase = createClient();
// Check ownership
const { data: existing } = await supabase
.from('posts')
.select('author_id')
.eq('id', args.id)
.single();
if (!existing || existing.author_id !== context.userId) {
throw new GraphQLError('Forbidden');
}
const { data, error } = await supabase
.from('posts')
.update(args.input)
.eq('id', args.id)
.select()
.single();
if (error) throw new GraphQLError(error.message);
return data;
},
deletePost: async (_: unknown, args: { id: string }, context: { userId?: string }) => {
if (!context.userId) {
throw new GraphQLError('Unauthorized');
}
const supabase = createClient();
const { error } = await supabase
.from('posts')
.delete()
.eq('id', args.id)
.eq('author_id', context.userId);
if (error) throw new GraphQLError(error.message);
return true;
},
},
// Field resolvers for relationships
Post: {
author: async (post: { author_id: string }) => {
const supabase = createClient();
const { data } = await supabase
.from('profiles')
.select('*')
.eq('id', post.author_id)
.single();
return data;
},
comments: async (post: { id: string }) => {
const supabase = createClient();
const { data } = await supabase
.from('comments')
.select('*')
.eq('post_id', post.id)
.order('created_at', { ascending: true });
return data ?? [];
},
},
User: {
posts: async (user: { id: string }) => {
const supabase = createClient();
const { data } = await supabase
.from('posts')
.select('*')
.eq('author_id', user.id);
return data ?? [];
},
},
};
GraphQL API Route // app/api/graphql/route.ts
import { ApolloServer } from '@apollo/server';
import { startServerAndCreateNextHandler } from '@as-integrations/next';
import { NextRequest } from 'next/server';
import { typeDefs } from '@/lib/graphql/schema';
import { resolvers } from '@/lib/graphql/resolvers';
import { createClient } from '@/lib/supabase/server';
const server = new ApolloServer({
typeDefs,
resolvers,
});
const handler = startServerAndCreateNextHandler(server, {
context: async (req: NextRequest) => {
const supabase = createClient();
const { data: { user } } = await supabase.auth.getUser();
return {
userId: user?.id,
};
},
});
export { handler as GET, handler as POST };
tRPC for Full-Stack TypeScript
Setting Up tRPC npm install @trpc/server @trpc/client @trpc/react-query @trpc/next
tRPC Router // lib/trpc/router.ts
import { initTRPC, TRPCError } from '@trpc/server';
import { z } from 'zod';
import { createClient } from '@/lib/supabase/server';
import superjson from 'superjson';
const t = initTRPC.context<{ userId?: string }>().create({
transformer: superjson,
});
const publicProcedure = t.procedure;
const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.userId) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next({ ctx: { ...ctx, userId: ctx.userId } });
});
export const appRouter = t.router({
// Public queries
posts: t.router({
list: publicProcedure
.input(z.object({
limit: z.number().min(1).max(100).default(10),
cursor: z.string().nullish(),
published: z.boolean().optional(),
}))
.query(async ({ input }) => {
const supabase = createClient();
let query = supabase
.from('posts')
.select('*, author:profiles(username, avatar_url)')
.order('created_at', { ascending: false })
.limit(input.limit + 1);
if (input.published !== undefined) {
query = query.eq('published', input.published);
}
if (input.cursor) {
query = query.lt('created_at', input.cursor);
}
const { data, error } = await query;
if (error) throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: error.message });
let nextCursor: string | undefined;
if (data.length > input.limit) {
const nextItem = data.pop();
nextCursor = nextItem?.created_at;
}
return { items: data, nextCursor };
}),
byId: publicProcedure
.input(z.object({ id: z.string().uuid() }))
.query(async ({ input }) => {
const supabase = createClient();
const { data, error } = await supabase
.from('posts')
.select('*, author:profiles(username, avatar_url)')
.eq('id', input.id)
.single();
if (error) throw new TRPCError({ code: 'NOT_FOUND' });
return data;
}),
// Protected mutations
create: protectedProcedure
.input(z.object({
title: z.string().min(1).max(200),
content: z.string().min(1),
published: z.boolean().default(false),
}))
.mutation(async ({ ctx, input }) => {
const supabase = createClient();
const { data, error } = await supabase
.from('posts')
.insert({
...input,
author_id: ctx.userId,
slug: input.title.toLowerCase().replace(/\s+/g, '-'),
})
.select()
.single();
if (error) throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: error.message });
return data;
}),
update: protectedProcedure
.input(z.object({
id: z.string().uuid(),
title: z.string().min(1).max(200).optional(),
content: z.string().min(1).optional(),
published: z.boolean().optional(),
}))
.mutation(async ({ ctx, input }) => {
const supabase = createClient();
const { id, ...updateData } = input;
const { data, error } = await supabase
.from('posts')
.update(updateData)
.eq('id', id)
.eq('author_id', ctx.userId)
.select()
.single();
if (error) throw new TRPCError({ code: 'FORBIDDEN' });
return data;
}),
delete: protectedProcedure
.input(z.object({ id: z.string().uuid() }))
.mutation(async ({ ctx, input }) => {
const supabase = createClient();
const { error } = await supabase
.from('posts')
.delete()
.eq('id', input.id)
.eq('author_id', ctx.userId);
if (error) throw new TRPCError({ code: 'FORBIDDEN' });
return { success: true };
}),
}),
});
export type AppRouter = typeof appRouter;
API Documentation
OpenAPI/Swagger Generation You: "Generate OpenAPI spec for the posts API"
# openapi.yaml
openapi: 3.0.3
info:
title: Blog API
version: 1.0.0
description: REST API for blog posts management
servers:
- url: https://api.example.com/v1
description: Production
- url: http://localhost:3000/api
description: Development
paths:
/posts:
get:
summary: List all posts
tags: [Posts]
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: limit
in: query
schema:
type: integer
default: 10
maximum: 100
- name: published
in: query
schema:
type: boolean
responses:
'200':
description: Paginated list of posts
content:
application/json:
schema:
$ref: '#/components/schemas/PostListResponse'
post:
summary: Create a new post
tags: [Posts]
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreatePostInput'
responses:
'201':
description: Post created
content:
application/json:
schema:
$ref: '#/components/schemas/Post'
'401':
$ref: '#/components/responses/Unauthorized'
components:
schemas:
Post:
type: object
properties:
id:
type: string
format: uuid
title:
type: string
slug:
type: string
content:
type: string
published:
type: boolean
created_at:
type: string
format: date-time
author:
$ref: '#/components/schemas/Author'
CreatePostInput:
type: object
required: [title, content]
properties:
title:
type: string
minLength: 1
maxLength: 200
content:
type: string
minLength: 1
published:
type: boolean
default: false
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
Conclusion Google Antigravity accelerates API development with:
Complete CRUD endpoint generation
Type-safe schemas and validation
Authentication/authorization patterns
Auto-generated documentation
Use Zod for runtime validation
Implement proper error handling
Add authentication at the middleware level
Generate OpenAPI docs for REST APIs
Consider tRPC for full-stack TypeScript
Build APIs faster with AI assistance. Focus on business logic, not boilerplate.