Skip to content

API Route Scanning ​

Learn how Carla Next.js automatically discovers and analyzes your Next.js API routes using TypeScript AST parsing.

What is API Route Scanning? ​

API route scanning is the first step in the tool generation pipeline. The scanner:

  • Discovers all API route files in your Next.js project
  • Analyzes the code structure using TypeScript's Abstract Syntax Tree (AST)
  • Extracts metadata about endpoints, parameters, and authentication
  • Detects both App Router and Pages Router patterns

Quick Start ​

Scan your project with a single command:

bash
npx @interworky/carla-nextjs scan

This will:

  1. Find all API route files
  2. Parse and analyze each file
  3. Generate tool definitions
  4. Save results to .carla/tools.json

Supported Route Patterns ​

The scanner automatically detects routes in these locations:

App Router (Next.js 13+) ​

app/api/**/route.{ts,js,tsx,jsx}
src/app/api/**/route.{ts,js,tsx,jsx}

Examples:

app/api/users/route.ts
app/api/products/[id]/route.ts
app/api/organizations/[orgId]/members/route.ts

Pages Router (Next.js 12 and earlier) ​

pages/api/**/*.{ts,js,tsx,jsx}
src/pages/api/**/*.{ts,js,tsx,jsx}

Examples:

pages/api/users.ts
pages/api/products/[id].ts
pages/api/auth/[...nextauth].ts

TIP

The scanner works with both TypeScript and JavaScript files, but TypeScript provides better type inference.

How Scanning Works ​

1. File Discovery ​

The scanner uses glob patterns to find all API route files:

typescript
const patterns = [
  'app/api/**/route.{ts,js,tsx,jsx}',
  'src/app/api/**/route.{ts,js,tsx,jsx}',
  'pages/api/**/*.{ts,js,tsx,jsx}',
  'src/pages/api/**/*.{ts,js,tsx,jsx}',
];

for (const pattern of patterns) {
  const files = await glob(pattern, { cwd: projectRoot });
  // Process each file...
}

2. TypeScript AST Parsing ​

Each file is parsed into an Abstract Syntax Tree:

typescript
import * as ts from 'typescript';

const source = fs.readFileSync(filePath, 'utf-8');
const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true);

3. Method Extraction ​

The scanner looks for exported HTTP method functions:

typescript
// These functions are detected
export async function GET(request: Request) {}
export async function POST(request: Request) {}
export async function PUT(request: Request) {}
export async function DELETE(request: Request) {}
export async function PATCH(request: Request) {}

4. Metadata Extraction ​

For each method, the scanner extracts:

  • Endpoint path (from file structure)
  • Path parameters (from [param] segments)
  • Body parameters (from request.json() calls)
  • Authentication (from common auth patterns)

Route Information Structure ​

The scanner produces a RouteInfo object for each file:

typescript
interface RouteInfo {
  filePath: string; // Absolute path to file
  relativePath: string; // Path relative to project root
  methods: MethodInfo[]; // Detected HTTP methods
}

interface MethodInfo {
  method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
  endpoint: string; // API endpoint (e.g., "/api/users/:id")
  params: ParamInfo[]; // Path parameters
  bodyParams: ParamInfo[]; // Request body parameters
  hasAuth: boolean; // Whether auth was detected
}

interface ParamInfo {
  name: string; // Parameter name
  type: string; // TypeScript type
  required: boolean; // Whether parameter is required
  description?: string; // Auto-generated description
}

Endpoint Path Conversion ​

The scanner converts file paths to API endpoints:

App Router Examples ​

File PathConverted Endpoint
app/api/users/route.ts/api/users
app/api/users/[id]/route.ts/api/users/:id
app/api/posts/[postId]/comments/route.ts/api/posts/:postId/comments
src/app/api/products/route.ts/api/products

Pages Router Examples ​

File PathConverted Endpoint
pages/api/users.ts/api/users
pages/api/users/[id].ts/api/users/:id
pages/api/orders/index.ts/api/orders

Conversion Logic ​

typescript
function pathToEndpoint(relativePath: string): string {
  let endpoint = relativePath
    .replace(/\.(ts|js|tsx|jsx)$/, '') // Remove extension
    .replace(/\/route$/, '') // Remove "route" filename
    .replace(/\/index$/, ''); // Remove "index" filename

  // Remove app/api or pages/api prefix
  endpoint = endpoint.replace(/^(src\/)?app\/api\//, '/').replace(/^(src\/)?pages\/api\//, '/');

  // Convert [param] to :param
  endpoint = endpoint.replace(/\[([^\]]+)\]/g, ':$1');

  // Ensure it starts with /api
  if (!endpoint.startsWith('/api')) {
    endpoint = '/api' + endpoint;
  }

  return endpoint || '/api';
}

Parameter Detection ​

Path Parameters ​

Path parameters are extracted from the file structure:

typescript
// File: app/api/products/[productId]/reviews/[reviewId]/route.ts
// Detected params: productId, reviewId
json
{
  "params": [
    {
      "name": "productId",
      "type": "string",
      "required": true,
      "description": "The productId parameter"
    },
    {
      "name": "reviewId",
      "type": "string",
      "required": true,
      "description": "The reviewId parameter"
    }
  ]
}

Body Parameters ​

The scanner analyzes code to detect request body parameters:

Pattern 1: Direct destructuring with types

typescript
export async function POST(request: Request) {
  const { name, email }: { name: string; email: string } = await request.json();
}

Pattern 2: Variable assignment

typescript
export async function POST(request: Request) {
  const body = await request.json();
  const { title, content } = body;
}

Pattern 3: Type interfaces

typescript
interface CreateUserRequest {
  name: string;
  email: string;
  age?: number;
}

export async function POST(request: Request) {
  const data: CreateUserRequest = await request.json();
}

TIP

For best results, use inline type annotations with destructuring. This provides the clearest signal to the scanner.

Authentication Detection ​

The scanner looks for common authentication patterns in the code:

NextAuth.js ​

typescript
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';

export async function GET(request: Request) {
  const session = await getServerSession(authOptions);
  if (!session) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }
  // ...
}

Custom JWT Verification ​

typescript
export async function POST(request: Request) {
  const token = request.headers.get('Authorization');
  const verified = await verifyToken(token);
  if (!verified) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }
  // ...
}

Middleware Authentication ​

typescript
export async function DELETE(request: Request) {
  await requireAuth(request);
  // ...
}

Detected Patterns ​

The scanner recognizes these auth-related keywords:

  • getServerSession
  • auth()
  • authenticate
  • verifyToken
  • checkAuth
  • requireAuth
  • Authorization (header)
  • Bearer (token)

When any of these are found, hasAuth: true is set on the method.

Scan Command Options ​

Force Rescan ​

By default, scanning is skipped if tools already exist:

bash
# Force a fresh scan
npx @interworky/carla-nextjs scan --force

Custom Path ​

Scan a specific directory:

bash
npx @interworky/carla-nextjs scan --path ./src/app/api

Output Format ​

Get JSON output for programmatic use:

bash
npx @interworky/carla-nextjs scan --format json

Output:

json
{
  "success": true,
  "filesScanned": 12,
  "toolsGenerated": 18,
  "toolsEnabled": 17,
  "toolsDisabled": 1,
  "tools": [...],
  "recommendations": [...]
}

Examples ​

Example 1: Simple GET Route ​

File: app/api/products/route.ts

typescript
export async function GET() {
  const products = await db.products.findMany();
  return Response.json(products);
}

Scanned info:

json
{
  "filePath": "/project/app/api/products/route.ts",
  "relativePath": "app/api/products/route.ts",
  "methods": [
    {
      "method": "GET",
      "endpoint": "/api/products",
      "params": [],
      "bodyParams": [],
      "hasAuth": false
    }
  ]
}

Example 2: Dynamic Route with Auth ​

File: app/api/users/[userId]/route.ts

typescript
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';

export async function GET(request: Request, { params }: { params: { userId: string } }) {
  const session = await getServerSession(authOptions);
  if (!session) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const user = await db.users.findUnique({
    where: { id: params.userId },
  });

  return Response.json(user);
}

export async function DELETE(request: Request, { params }: { params: { userId: string } }) {
  const session = await getServerSession(authOptions);
  if (!session) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }

  await db.users.delete({
    where: { id: params.userId },
  });

  return Response.json({ success: true });
}

Scanned info:

json
{
  "filePath": "/project/app/api/users/[userId]/route.ts",
  "relativePath": "app/api/users/[userId]/route.ts",
  "methods": [
    {
      "method": "GET",
      "endpoint": "/api/users/:userId",
      "params": [
        {
          "name": "userId",
          "type": "string",
          "required": true,
          "description": "The userId parameter"
        }
      ],
      "bodyParams": [],
      "hasAuth": true
    },
    {
      "method": "DELETE",
      "endpoint": "/api/users/:userId",
      "params": [
        {
          "name": "userId",
          "type": "string",
          "required": true,
          "description": "The userId parameter"
        }
      ],
      "bodyParams": [],
      "hasAuth": true
    }
  ]
}

Example 3: POST with Body Parameters ​

File: app/api/orders/route.ts

typescript
export async function POST(request: Request) {
  const {
    customerId,
    items,
    shippingAddress,
  }: {
    customerId: string;
    items: Array<{ productId: string; quantity: number }>;
    shippingAddress: string;
  } = await request.json();

  const order = await db.orders.create({
    data: {
      customerId,
      items,
      shippingAddress,
      status: 'pending',
    },
  });

  return Response.json(order, { status: 201 });
}

Scanned info:

json
{
  "filePath": "/project/app/api/orders/route.ts",
  "relativePath": "app/api/orders/route.ts",
  "methods": [
    {
      "method": "POST",
      "endpoint": "/api/orders",
      "params": [],
      "bodyParams": [
        {
          "name": "customerId",
          "type": "string",
          "required": true,
          "description": "The customerId field"
        },
        {
          "name": "items",
          "type": "array",
          "required": true,
          "description": "The items field"
        },
        {
          "name": "shippingAddress",
          "type": "string",
          "required": true,
          "description": "The shippingAddress field"
        }
      ],
      "hasAuth": false
    }
  ]
}

TypeScript AST Deep Dive ​

AST Node Types ​

The scanner looks for specific TypeScript node types:

typescript
// Function declarations
ts.isFunctionDeclaration(node);

// Await expressions (for request.json())
ts.isAwaitExpression(node);

// Call expressions (for function calls)
ts.isCallExpression(node);

// Variable declarations (for const/let/var)
ts.isVariableDeclaration(node);

Walking the AST ​

The scanner recursively traverses the syntax tree:

typescript
function visit(node: ts.Node) {
  // Check if this is an HTTP method function
  if (ts.isFunctionDeclaration(node) && node.name) {
    const methodName = node.name.text;
    if (['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].includes(methodName)) {
      // Extract method information
      extractMethodInfo(node);
    }
  }

  // Continue traversing child nodes
  ts.forEachChild(node, visit);
}

visit(sourceFile);

Example AST Analysis ​

Source code:

typescript
export async function GET(request: Request) {
  const data = await request.json();
  return Response.json(data);
}

AST structure:

SourceFile
└── FunctionDeclaration (name: "GET")
    ├── Modifier (export)
    ├── Modifier (async)
    ├── Parameter (name: "request", type: Request)
    └── Block
        ├── VariableDeclaration (name: "data")
        │   └── AwaitExpression
        │       └── CallExpression (request.json)
        └── ReturnStatement
            └── CallExpression (Response.json)

Best Practices ​

1. Use Consistent File Structure ​

typescript
// ✅ Good - Clear structure
app / api / users / route.ts;
app / api / users / [id] / route.ts;
app / api / products / route.ts;
app / api / products / [id] / route.ts;

// ❌ Confusing - Mixed patterns
app / api / getUsers.ts;
app / api / user - details / [id].ts;

2. Add Type Annotations ​

typescript
// ✅ Good - Scanner can detect parameters
const { name, email }: { name: string; email: string } = await request.json();

// ❌ Limited - Scanner may miss parameters
const body = await request.json();
const name = body.name;

3. Use Standard HTTP Methods ​

typescript
// ✅ Good - Standard methods
export async function GET() {}
export async function POST() {}

// ❌ Won't be detected
export async function getUsers() {}
export async function fetchData() {}

4. Keep Auth Patterns Visible ​

typescript
// ✅ Good - Auth is in the same function
export async function POST(request: Request) {
  const session = await getServerSession(authOptions);
  if (!session) return Response.json({ error: 'Unauthorized' }, { status: 401 });
  // ...
}

// ❌ May not be detected
async function checkAuth(request: Request) {}

export async function POST(request: Request) {
  checkAuth(request); // Auth in separate function
  // ...
}

5. Avoid Dynamic Routes ​

typescript
// ✅ Good - Static, scannable
app/api/users/[id]/route.ts

// ❌ Can't be scanned
app/api/[...catch-all]/route.ts
app/api/[[...optional]]/route.ts

Troubleshooting ​

No Routes Found ​

Problem: Scanner reports 0 API routes found

Solutions:

  1. Check your route files are in the correct location:

    app/api/  or  pages/api/
  2. Verify file naming:

    • App Router: Files must be named route.ts/js
    • Pages Router: Any .ts/.js filename works
  3. Check for TypeScript/JavaScript syntax errors

  4. Run from project root directory

Parameters Not Detected ​

Problem: Body parameters aren't showing up in scanned results

Solutions:

  1. Add explicit type annotations:

    typescript
    const { name }: { name: string } = await request.json();
  2. Use destructuring directly in the await statement

  3. Check for typos in request.json() call

Auth Not Detected ​

Problem: Authentication is implemented but hasAuth: false

Solutions:

  1. Use recognized auth patterns (see Authentication Detection)

  2. Ensure auth code is in the route handler function, not imported functions

  3. Manually add auth: "required" to tools.json after scanning

Multiple HTTP Methods ​

Problem: Only one method detected when multiple exist

Solutions:

  1. Ensure each method is a separate exported function:

    typescript
    export async function GET() {}
    export async function POST() {}
    export async function DELETE() {}
  2. Don't use conditional logic to handle multiple methods:

    typescript
    // ❌ Won't work
    export async function handler(request: Request) {
      if (request.method === 'GET') {
      }
      if (request.method === 'POST') {
      }
    }

Performance ​

Scan Speed ​

Typical scan times:

  • Small project (< 10 routes): < 1 second
  • Medium project (10-50 routes): 1-3 seconds
  • Large project (50+ routes): 3-10 seconds

Optimizations ​

The scanner is optimized for performance:

  1. Concurrent file reading - Files are read in parallel
  2. AST caching - TypeScript compiler results are cached
  3. Early termination - Stops analyzing when all methods are found
  4. Smart pattern matching - Only scans relevant directories

Next Steps ​

Now that you understand API scanning: