agprojects
      Tutorial
      {"antigravity"
      "api"
      "rest"
      "nextjs"
      "backend"}

      How to Create API Routes with Antigravity

      Build production-ready REST APIs with Antigravity. Learn authentication, validation, rate limiting, error handling, and best practices.

      By Projects.tech TeamNovember 20, 202522 min read78 views
      How to Create API Routes with Antigravity

      How to Create API Routes with Antigravity

      Overview

      Learn how to build production-ready REST APIs with Antigravity in minutes. This guide covers Next.js API routes, server actions, authentication, validation, error handling, and best practices.

      What You'll Learn

      • Creating RESTful API endpoints
      • Request validation with Zod
      • Authentication and authorization
      • Error handling patterns
      • Rate limiting
      • API documentation
      • Testing endpoints
      • Server Actions vs API Routes

      Prerequisites

      • Next.js 14 project
      • Antigravity editor
      • Basic REST API knowledge

      API Routes vs Server Actions

      API Routes

      • Traditional REST endpoints
      • Accessible from any client
      • Can be called from external apps
      • Support all HTTP methods

      Server Actions

      • Next.js 14 feature
      • Type-safe client-server communication
      • Only accessible from React components
      • Automatic serialization
      • Better DX for form handling

      When to use each:

      • API Routes: Public APIs, webhooks, external integrations
      • Server Actions: Internal app logic, form submissions, mutations

      Creating Your First API

      Step 1: Start with a Prompt

      Create a REST API for a task management system with:
      
      ENDPOINTS:
      - GET /api/tasks (list all tasks, with pagination)
      - GET /api/tasks/[id] (get single task)
      - POST /api/tasks (create new task)
      - PUT /api/tasks/[id] (update task)
      - DELETE /api/tasks/[id] (delete task)
      - GET /api/tasks/stats (task statistics)
      
      FEATURES:
      - JWT authentication (protect all routes)
      - Request validation with Zod
      - Error handling with proper status codes
      - Rate limiting (100 requests/hour per user)
      - Pagination (default 10, max 100 per page)
      - Filtering (status, priority, assignee)
      - Sorting (createdAt, dueDate, priority)
      
      DATA MODEL:
      Task {
        id: UUID
        title: string (required, max 200 chars)
        description: string (optional)
        status: enum (TODO, IN_PROGRESS, DONE)
        priority: enum (LOW, MEDIUM, HIGH)
        dueDate: datetime (optional)
        assigneeId: UUID
        createdAt: datetime
        updatedAt: datetime
      }
      
      RESPONSE FORMAT:
      Success: {
        success: true,
        data: {...},
        message: "Success message"
      }
      
      Error: {
        success: false,
        error: "Error message",
        code: "ERROR_CODE"
      }
      
      TECH STACK:
      - Next.js 14 API Routes
      - Prisma ORM
      - Zod validation
      - JWT (jsonwebtoken)
      - Rate limiting (upstash/ratelimit)
      

      Step 2: Implementation

      Antigravity generates:

      File Structure

      app/
      ├── api/
      │   ├── tasks/
      │   │   ├── route.ts (GET, POST)
      │   │   ├── [id]/
      │   │   │   └── route.ts (GET, PUT, DELETE)
      │   │   └── stats/
      │   │       └── route.ts (GET)
      │   └── auth/
      │       └── middleware.ts
      lib/
      ├── validations/
      │   └── task.ts (Zod schemas)
      ├── auth.ts (JWT helpers)
      ├── errors.ts (Error classes)
      └── ratelimit.ts (Rate limit config)
      

      GET /api/tasks (List)

      // app/api/tasks/route.ts
      import { NextRequest, NextResponse } from 'next/server';
      import { prisma } from '@/lib/prisma';
      import { verifyToken } from '@/lib/auth';
      import { rateLimit } from '@/lib/ratelimit';
      import { TaskQuerySchema } from '@/lib/validations/task';
      
      export async function GET(request: NextRequest) {
        try {
          // Authenticate
          const user = await verifyToken(request);
          if (!user) {
            return NextResponse.json(
              { success: false, error: 'Unauthorized', code: 'AUTH_REQUIRED' },
              { status: 401 }
            );
          }
      
          // Rate limit
          const { success: rateLimitOk } = await rateLimit(user.id);
          if (!rateLimitOk) {
            return NextResponse.json(
              { success: false, error: 'Rate limit exceeded', code: 'RATE_LIMIT' },
              { status: 429 }
            );
          }
      
          // Parse and validate query params
          const { searchParams } = new URL(request.url);
          const queryParams = {
            page: parseInt(searchParams.get('page') || '1'),
            limit: parseInt(searchParams.get('limit') || '10'),
            status: searchParams.get('status'),
            priority: searchParams.get('priority'),
            assigneeId: searchParams.get('assigneeId'),
            sortBy: searchParams.get('sortBy') || 'createdAt',
            sortOrder: searchParams.get('sortOrder') || 'desc',
          };
      
          const validated = TaskQuerySchema.parse(queryParams);
      
          // Build where clause
          const where: any = {};
          if (validated.status) where.status = validated.status;
          if (validated.priority) where.priority = validated.priority;
          if (validated.assigneeId) where.assigneeId = validated.assigneeId;
      
          // Query with pagination
          const [tasks, total] = await Promise.all([
            prisma.task.findMany({
              where,
              skip: (validated.page - 1) * validated.limit,
              take: validated.limit,
              orderBy: { [validated.sortBy]: validated.sortOrder },
              include: {
                assignee: {
                  select: { id: true, name: true, email: true }
                }
              }
            }),
            prisma.task.count({ where })
          ]);
      
          return NextResponse.json({
            success: true,
            data: tasks,
            pagination: {
              page: validated.page,
              limit: validated.limit,
              total,
              totalPages: Math.ceil(total / validated.limit)
            }
          });
      
        } catch (error) {
          if (error instanceof z.ZodError) {
            return NextResponse.json(
              {
                success: false,
                error: 'Validation failed',
                code: 'VALIDATION_ERROR',
                issues: error.errors
              },
              { status: 400 }
            );
          }
      
          console.error('GET /api/tasks error:', error);
          return NextResponse.json(
            { success: false, error: 'Internal server error', code: 'SERVER_ERROR' },
            { status: 500 }
          );
        }
      }
      

      POST /api/tasks (Create)

      export async function POST(request: NextRequest) {
        try {
          const user = await verifyToken(request);
          if (!user) {
            return NextResponse.json(
              { success: false, error: 'Unauthorized', code: 'AUTH_REQUIRED' },
              { status: 401 }
            );
          }
      
          const body = await request.json();
          const validated = TaskCreateSchema.parse(body);
      
          const task = await prisma.task.create({
            data: {
              ...validated,
              assigneeId: user.id,
            },
            include: {
              assignee: {
                select: { id: true, name: true, email: true }
              }
            }
          });
      
          return NextResponse.json(
            {
              success: true,
              data: task,
              message: 'Task created successfully'
            },
            { status: 201 }
          );
      
        } catch (error) {
          if (error instanceof z.ZodError) {
            return NextResponse.json(
              {
                success: false,
                error: 'Validation failed',
                code: 'VALIDATION_ERROR',
                issues: error.errors
              },
              { status: 400 }
            );
          }
      
          console.error('POST /api/tasks error:', error);
          return NextResponse.json(
            { success: false, error: 'Internal server error', code: 'SERVER_ERROR' },
            { status: 500 }
          );
        }
      }
      

      PUT /api/tasks/[id] (Update)

      // app/api/tasks/[id]/route.ts
      export async function PUT(
        request: NextRequest,
        { params }: { params: { id: string } }
      ) {
        try {
          const user = await verifyToken(request);
          if (!user) {
            return NextResponse.json(
              { success: false, error: 'Unauthorized', code: 'AUTH_REQUIRED' },
              { status: 401 }
            );
          }
      
          // Check task exists and user has permission
          const existingTask = await prisma.task.findUnique({
            where: { id: params.id }
          });
      
          if (!existingTask) {
            return NextResponse.json(
              { success: false, error: 'Task not found', code: 'NOT_FOUND' },
              { status: 404 }
            );
          }
      
          if (existingTask.assigneeId !== user.id && user.role !== 'ADMIN') {
            return NextResponse.json(
              { success: false, error: 'Forbidden', code: 'FORBIDDEN' },
              { status: 403 }
            );
          }
      
          const body = await request.json();
          const validated = TaskUpdateSchema.parse(body);
      
          const task = await prisma.task.update({
            where: { id: params.id },
            data: validated,
            include: {
              assignee: {
                select: { id: true, name: true, email: true }
              }
            }
          });
      
          return NextResponse.json({
            success: true,
            data: task,
            message: 'Task updated successfully'
          });
      
        } catch (error) {
          // Error handling...
        }
      }
      

      DELETE /api/tasks/[id]

      export async function DELETE(
        request: NextRequest,
        { params }: { params: { id: string } }
      ) {
        try {
          const user = await verifyToken(request);
          if (!user) {
            return NextResponse.json(
              { success: false, error: 'Unauthorized', code: 'AUTH_REQUIRED' },
              { status: 401 }
            );
          }
      
          const existingTask = await prisma.task.findUnique({
            where: { id: params.id }
          });
      
          if (!existingTask) {
            return NextResponse.json(
              { success: false, error: 'Task not found', code: 'NOT_FOUND' },
              { status: 404 }
            );
          }
      
          if (existingTask.assigneeId !== user.id && user.role !== 'ADMIN') {
            return NextResponse.json(
              { success: false, error: 'Forbidden', code: 'FORBIDDEN' },
              { status: 403 }
            );
          }
      
          await prisma.task.delete({
            where: { id: params.id }
          });
      
          return NextResponse.json({
            success: true,
            message: 'Task deleted successfully'
          });
      
        } catch (error) {
          // Error handling...
        }
      }
      

      Step 3: Validation Schemas

      // lib/validations/task.ts
      import { z } from 'zod';
      
      export const TaskCreateSchema = z.object({
        title: z.string()
          .min(1, 'Title is required')
          .max(200, 'Title must not exceed 200 characters'),
      
        description: z.string().optional(),
      
        status: z.enum(['TODO', 'IN_PROGRESS', 'DONE']).default('TODO'),
      
        priority: z.enum(['LOW', 'MEDIUM', 'HIGH']).default('MEDIUM'),
      
        dueDate: z.string().datetime().optional(),
      });
      
      export const TaskUpdateSchema = TaskCreateSchema.partial();
      
      export const TaskQuerySchema = z.object({
        page: z.number().int().positive().default(1),
        limit: z.number().int().min(1).max(100).default(10),
        status: z.enum(['TODO', 'IN_PROGRESS', 'DONE']).optional(),
        priority: z.enum(['LOW', 'MEDIUM', 'HIGH']).optional(),
        assigneeId: z.string().uuid().optional(),
        sortBy: z.enum(['createdAt', 'dueDate', 'priority']).default('createdAt'),
        sortOrder: z.enum(['asc', 'desc']).default('desc'),
      });
      

      Step 4: Authentication

      // lib/auth.ts
      import { NextRequest } from 'next/server';
      import jwt from 'jsonwebtoken';
      
      const JWT_SECRET = process.env.JWT_SECRET!;
      
      export async function verifyToken(request: NextRequest) {
        const authHeader = request.headers.get('authorization');
      
        if (!authHeader || !authHeader.startsWith('Bearer ')) {
          return null;
        }
      
        const token = authHeader.substring(7);
      
        try {
          const decoded = jwt.verify(token, JWT_SECRET) as {
            userId: string;
            email: string;
            role: string;
          };
      
          return decoded;
        } catch (error) {
          return null;
        }
      }
      
      export function generateToken(user: { id: string; email: string; role: string }) {
        return jwt.sign(
          { userId: user.id, email: user.email, role: user.role },
          JWT_SECRET,
          { expiresIn: '7d' }
        );
      }
      

      Step 5: Rate Limiting

      // lib/ratelimit.ts
      import { Ratelimit } from '@upstash/ratelimit';
      import { Redis } from '@upstash/redis';
      
      const redis = new Redis({
        url: process.env.UPSTASH_REDIS_REST_URL!,
        token: process.env.UPSTASH_REDIS_REST_TOKEN!,
      });
      
      export const rateLimit = new Ratelimit({
        redis,
        limiter: Ratelimit.slidingWindow(100, '1 h'),
        analytics: true,
      });
      

      Advanced API Patterns

      1. Nested Resources

      GET /api/users/[userId]/tasks
      POST /api/users/[userId]/tasks
      GET /api/projects/[projectId]/tasks
      
      // app/api/users/[userId]/tasks/route.ts
      export async function GET(
        request: NextRequest,
        { params }: { params: { userId: string } }
      ) {
        const tasks = await prisma.task.findMany({
          where: { assigneeId: params.userId }
        });
      
        return NextResponse.json({ success: true, data: tasks });
      }
      

      2. Batch Operations

      // app/api/tasks/batch/route.ts
      export async function POST(request: NextRequest) {
        const { operation, ids } = await request.json();
      
        if (operation === 'delete') {
          await prisma.task.deleteMany({
            where: { id: { in: ids } }
          });
        } else if (operation === 'update') {
          await prisma.task.updateMany({
            where: { id: { in: ids } },
            data: { status: 'DONE' }
          });
        }
      
        return NextResponse.json({
          success: true,
          message: `${operation} completed for ${ids.length} tasks`
        });
      }
      

      3. File Upload

      // app/api/upload/route.ts
      export async function POST(request: NextRequest) {
        const formData = await request.formData();
        const file = formData.get('file') as File;
      
        if (!file) {
          return NextResponse.json(
            { success: false, error: 'No file provided' },
            { status: 400 }
          );
        }
      
        // Validate file
        const MAX_SIZE = 5 * 1024 * 1024; // 5MB
        if (file.size > MAX_SIZE) {
          return NextResponse.json(
            { success: false, error: 'File too large (max 5MB)' },
            { status: 400 }
          );
        }
      
        const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
        if (!allowedTypes.includes(file.type)) {
          return NextResponse.json(
            { success: false, error: 'Invalid file type' },
            { status: 400 }
          );
        }
      
        // Upload to storage (e.g., S3, UploadThing)
        const uploadResult = await uploadFile(file);
      
        return NextResponse.json({
          success: true,
          data: { url: uploadResult.url }
        });
      }
      

      4. Webhooks

      // app/api/webhooks/stripe/route.ts
      import { headers } from 'next/headers';
      import Stripe from 'stripe';
      
      const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
      
      export async function POST(request: NextRequest) {
        const body = await request.text();
        const signature = headers().get('stripe-signature')!;
      
        let event: Stripe.Event;
      
        try {
          event = stripe.webhooks.constructEvent(
            body,
            signature,
            process.env.STRIPE_WEBHOOK_SECRET!
          );
        } catch (error) {
          return NextResponse.json(
            { success: false, error: 'Invalid signature' },
            { status: 400 }
          );
        }
      
        // Handle event
        switch (event.type) {
          case 'payment_intent.succeeded':
            const paymentIntent = event.data.object;
            await handlePaymentSuccess(paymentIntent);
            break;
      
          case 'customer.subscription.deleted':
            const subscription = event.data.object;
            await handleSubscriptionCanceled(subscription);
            break;
        }
      
        return NextResponse.json({ received: true });
      }
      

      5. Server-Sent Events (SSE)

      // app/api/events/route.ts
      export async function GET(request: NextRequest) {
        const encoder = new TextEncoder();
      
        const stream = new ReadableStream({
          async start(controller) {
            // Send initial message
            controller.enqueue(encoder.encode('data: Connected\n\n'));
      
            // Send updates every 5 seconds
            const interval = setInterval(() => {
              const data = { timestamp: new Date().toISOString() };
              controller.enqueue(
                encoder.encode(`data: ${JSON.stringify(data)}\n\n`)
              );
            }, 5000);
      
            // Cleanup on close
            request.signal.addEventListener('abort', () => {
              clearInterval(interval);
              controller.close();
            });
          },
        });
      
        return new Response(stream, {
          headers: {
            'Content-Type': 'text/event-stream',
            'Cache-Control': 'no-cache',
            'Connection': 'keep-alive',
          },
        });
      }
      

      Testing APIs

      With curl

      # GET with auth
      curl -H "Authorization: Bearer <token>" \
        http://localhost:3000/api/tasks
      
      # POST with JSON
      curl -X POST \
        -H "Authorization: Bearer <token>" \
        -H "Content-Type: application/json" \
        -d '{"title":"New Task","status":"TODO"}' \
        http://localhost:3000/api/tasks
      
      # PUT
      curl -X PUT \
        -H "Authorization: Bearer <token>" \
        -H "Content-Type: application/json" \
        -d '{"status":"DONE"}' \
        http://localhost:3000/api/tasks/123
      
      # DELETE
      curl -X DELETE \
        -H "Authorization: Bearer <token>" \
        http://localhost:3000/api/tasks/123
      

      With Jest

      // __tests__/api/tasks.test.ts
      import { POST } from '@/app/api/tasks/route';
      import { prisma } from '@/lib/prisma';
      import { generateToken } from '@/lib/auth';
      
      describe('POST /api/tasks', () => {
        let authToken: string;
      
        beforeAll(() => {
          authToken = generateToken({
            id: 'user-123',
            email: '[email protected]',
            role: 'USER'
          });
        });
      
        it('creates a task with valid data', async () => {
          const request = new Request('http://localhost:3000/api/tasks', {
            method: 'POST',
            headers: {
              'Authorization': `Bearer ${authToken}`,
              'Content-Type': 'application/json',
            },
            body: JSON.stringify({
              title: 'Test Task',
              status: 'TODO',
              priority: 'HIGH',
            }),
          });
      
          const response = await POST(request);
          const data = await response.json();
      
          expect(response.status).toBe(201);
          expect(data.success).toBe(true);
          expect(data.data.title).toBe('Test Task');
        });
      
        it('returns 401 without auth', async () => {
          const request = new Request('http://localhost:3000/api/tasks', {
            method: 'POST',
            body: JSON.stringify({ title: 'Test' }),
          });
      
          const response = await POST(request);
          expect(response.status).toBe(401);
        });
      
        it('validates required fields', async () => {
          const request = new Request('http://localhost:3000/api/tasks', {
            method: 'POST',
            headers: {
              'Authorization': `Bearer ${authToken}`,
              'Content-Type': 'application/json',
            },
            body: JSON.stringify({}),
          });
      
          const response = await POST(request);
          const data = await response.json();
      
          expect(response.status).toBe(400);
          expect(data.code).toBe('VALIDATION_ERROR');
        });
      });
      

      API Documentation

      Use comments for auto-documentation:

      /**
       * @swagger
       * /api/tasks:
       *   get:
       *     summary: List all tasks
       *     tags: [Tasks]
       *     security:
       *       - bearerAuth: []
       *     parameters:
       *       - in: query
       *         name: page
       *         schema:
       *           type: integer
       *         description: Page number
       *       - in: query
       *         name: status
       *         schema:
       *           type: string
       *           enum: [TODO, IN_PROGRESS, DONE]
       *     responses:
       *       200:
       *         description: Success
       *         content:
       *           application/json:
       *             schema:
       *               type: object
       *               properties:
       *                 success:
       *                   type: boolean
       *                 data:
       *                   type: array
       *                   items:
       *                     $ref: '#/components/schemas/Task'
       */
      export async function GET(request: NextRequest) {
        // Implementation
      }
      

      Error Handling Best Practices

      Custom Error Classes

      // lib/errors.ts
      export class AppError extends Error {
        constructor(
          public message: string,
          public statusCode: number,
          public code: string
        ) {
          super(message);
        }
      }
      
      export class ValidationError extends AppError {
        constructor(message: string) {
          super(message, 400, 'VALIDATION_ERROR');
        }
      }
      
      export class AuthenticationError extends AppError {
        constructor() {
          super('Unauthorized', 401, 'AUTH_REQUIRED');
        }
      }
      
      export class ForbiddenError extends AppError {
        constructor() {
          super('Forbidden', 403, 'FORBIDDEN');
        }
      }
      
      export class NotFoundError extends AppError {
        constructor(resource: string) {
          super(`${resource} not found`, 404, 'NOT_FOUND');
        }
      }
      

      Global Error Handler

      // lib/api-handler.ts
      export function withErrorHandling(handler: Function) {
        return async (request: NextRequest, context?: any) => {
          try {
            return await handler(request, context);
          } catch (error) {
            if (error instanceof AppError) {
              return NextResponse.json(
                {
                  success: false,
                  error: error.message,
                  code: error.code
                },
                { status: error.statusCode }
              );
            }
      
            if (error instanceof z.ZodError) {
              return NextResponse.json(
                {
                  success: false,
                  error: 'Validation failed',
                  code: 'VALIDATION_ERROR',
                  issues: error.errors
                },
                { status: 400 }
              );
            }
      
            console.error('Unhandled error:', error);
            return NextResponse.json(
              {
                success: false,
                error: 'Internal server error',
                code: 'SERVER_ERROR'
              },
              { status: 500 }
            );
          }
        };
      }
      
      // Usage
      export const GET = withErrorHandling(async (request: NextRequest) => {
        // Your handler code
      });
      

      Next Steps

      Master API development with:

      1. Build CRUD: How to Build CRUD with Antigravity
      2. Prisma Integration: How to Extend Prisma Schema
      3. Best Practices: Antigravity Best Practices
      4. Example Projects: Antigravity Projects Examples

      Conclusion

      Antigravity makes API development fast and maintainable:

      • Generate complete REST APIs in minutes
      • Built-in validation, auth, and error handling
      • Production-ready patterns
      • Type-safe end-to-end

      Start building APIs with Antigravity today!


      Keywords: antigravity api example, nextjs api with antigravity, create api routes, antigravity rest api, api tutorial antigravity, nextjs api routes

      Related Articles

      Tutorial

      Vibe Coding Sora AI: Copilot, Cursor & Prompt Engineering Tutorial

      1. Introduction: What Is Vibe Coding and Why It Matters 1.1 Defining "Vibe Coding" — From Natural Language to Working Software The term "vibe coding" was popula...

      October 28, 202610 min
      Tutorial

      Vibe Coding Tutorial: Sora AI, Copilot, Cursor & Prompt Engineering

      Introduction: Why Vibe Coding Is Changing How We Build Software Something fundamental has shifted in how developers write software. Instead of typing every line...

      October 21, 20269 min
      Tutorial

      AI Coding Tutorial: Vibe Coding with Copilot, Cursor & Google Antigravity

      Introduction: Why Vibe Coding Is Changing How We Build Software "Vibe coding" is the practice of describing what you want in plain natural language and letting ...

      September 30, 20268 min