# Minimal API Standards - .NET

> **Scope:** universal
> **Layer:** 0 (always load)
> **Keywords:** minimal api, dotnet, asp.net, endpoint, mapget, mappost
> **Load When:** always

Patterns and best practices for .NET Minimal API endpoints.

---

## Route Handler Structure

### File Organization

```
app/api/
  auth/
    login/
      route.ts         # POST /api/auth/login
    logout/
      route.ts         # POST /api/auth/logout
  users/
    route.ts           # GET /api/users, POST /api/users
    [id]/
      route.ts         # GET /api/users/:id, PATCH /api/users/:id
```

### HTTP Methods

```typescript
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server'

// GET /api/users
export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams
  const limit = searchParams.get('limit') || '10'

  // Fetch users
  return NextResponse.json({ users: [] })
}

// POST /api/users
export async function POST(request: NextRequest) {
  const body = await request.json()

  // Create user
  return NextResponse.json({ user: {} }, { status: 201 })
}
```

### Dynamic Routes

```typescript
// app/api/users/[id]/route.ts
interface RouteParams {
  params: { id: string }
}

export async function GET(
  request: NextRequest,
  { params }: RouteParams
) {
  const userId = params.id

  // Fetch user by ID
  return NextResponse.json({ user: {} })
}

export async function PATCH(
  request: NextRequest,
  { params }: RouteParams
) {
  const userId = params.id
  const updates = await request.json()

  // Update user
  return NextResponse.json({ user: {} })
}

export async function DELETE(
  request: NextRequest,
  { params }: RouteParams
) {
  const userId = params.id

  // Delete user
  return NextResponse.json(null, { status: 204 })
}
```

---

## Request Handling

### Query Parameters

```typescript
export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams
  const page = parseInt(searchParams.get('page') || '1')
  const limit = parseInt(searchParams.get('limit') || '10')
  const sort = searchParams.get('sort') || 'created_at'

  // Use query params
}
```

### Request Body Validation (Zod)

```typescript
import { z } from 'zod'

const createUserSchema = z.object({
  name: z.string().min(1, 'Name is required'),
  email: z.string().email('Invalid email'),
  age: z.number().int().positive().optional()
})

export async function POST(request: NextRequest) {
  try {
    const body = await request.json()
    const validatedData = createUserSchema.parse(body)

    // Proceed with validated data
    return NextResponse.json({ user: validatedData }, { status: 201 })
  } catch (error) {
    if (error instanceof z.ZodError) {
      return NextResponse.json(
        { error: 'Validation failed', details: error.errors },
        { status: 400 }
      )
    }
    throw error
  }
}
```

### Headers

```typescript
export async function GET(request: NextRequest) {
  const authHeader = request.headers.get('authorization')
  const contentType = request.headers.get('content-type')

  // Set response headers
  return NextResponse.json(
    { data: {} },
    {
      headers: {
        'Cache-Control': 'max-age=3600',
        'X-Custom-Header': 'value'
      }
    }
  )
}
```

---

## Response Patterns

### Success Responses

```typescript
// 200 OK - Successful GET
return NextResponse.json({ users: [] })

// 201 Created - Successful POST
return NextResponse.json({ user: {} }, { status: 201 })

// 204 No Content - Successful DELETE
return new NextResponse(null, { status: 204 })
```

### Error Responses

```typescript
// 400 Bad Request
return NextResponse.json(
  { error: 'Invalid input', details: errors },
  { status: 400 }
)

// 401 Unauthorized
return NextResponse.json(
  { error: 'Authentication required' },
  { status: 401 }
)

// 403 Forbidden
return NextResponse.json(
  { error: 'Insufficient permissions' },
  { status: 403 }
)

// 404 Not Found
return NextResponse.json(
  { error: 'Resource not found' },
  { status: 404 }
)

// 500 Internal Server Error
return NextResponse.json(
  { error: 'Internal server error' },
  { status: 500 }
)
```

---

## Authentication & Authorization

### Middleware-based Auth

```typescript
// middleware.ts
import { createMiddlewareClient } from '@/lib/supabase/middleware'
import { NextResponse } from 'next/server'

export async function middleware(request: NextRequest) {
  const res = NextResponse.next()
  const supabase = createMiddlewareClient(request, res)

  const { data: { session } } = await supabase.auth.getSession()

  // Protected API routes
  if (request.nextUrl.pathname.startsWith('/api/protected') && !session) {
    return NextResponse.json(
      { error: 'Unauthorized' },
      { status: 401 }
    )
  }

  return res
}
```

### Route-level Auth

```typescript
import { createRouteHandlerClient } from '@/lib/supabase/server'

export async function GET(request: NextRequest) {
  const supabase = createRouteHandlerClient()
  const { data: { session } } = await supabase.auth.getSession()

  if (!session) {
    return NextResponse.json(
      { error: 'Unauthorized' },
      { status: 401 }
    )
  }

  // Proceed with authenticated request
  const { data: profile } = await supabase
    .from('profiles')
    .select('*')
    .eq('user_id', session.user.id)
    .single()

  return NextResponse.json({ profile })
}
```

### Role-based Authorization

```typescript
const ADMIN_ROLES = ['admin', 'super_admin']

export async function DELETE(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const supabase = createRouteHandlerClient()
  const { data: { session } } = await supabase.auth.getSession()

  if (!session) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  // Check user role
  const { data: profile } = await supabase
    .from('profiles')
    .select('role')
    .eq('user_id', session.user.id)
    .single()

  if (!profile || !ADMIN_ROLES.includes(profile.role)) {
    return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
  }

  // Proceed with admin-only operation
}
```

---

## Error Handling

### Centralized Error Handler

```typescript
// lib/api/errorHandler.ts
import { NextResponse } from 'next/server'
import { PostgrestError } from '@supabase/supabase-js'
import { z } from 'zod'

export function handleApiError(error: unknown) {
  console.error('API Error:', error)

  // Zod validation errors
  if (error instanceof z.ZodError) {
    return NextResponse.json(
      { error: 'Validation failed', details: error.errors },
      { status: 400 }
    )
  }

  // Supabase errors
  if (isPostgrestError(error)) {
    return NextResponse.json(
      { error: error.message, code: error.code },
      { status: 400 }
    )
  }

  // Default error
  return NextResponse.json(
    { error: 'Internal server error' },
    { status: 500 }
  )
}

function isPostgrestError(error: unknown): error is PostgrestError {
  return (error as PostgrestError).code !== undefined
}
```

### Usage

```typescript
import { handleApiError } from '@/lib/api/errorHandler'

export async function POST(request: NextRequest) {
  try {
    // API logic
    return NextResponse.json({ success: true })
  } catch (error) {
    return handleApiError(error)
  }
}
```

---

## Supabase Integration

### Database Operations

```typescript
import { createRouteHandlerClient } from '@/lib/supabase/server'

export async function GET(request: NextRequest) {
  const supabase = createRouteHandlerClient()

  const { data: users, error } = await supabase
    .from('users')
    .select('id, name, email')
    .order('created_at', { ascending: false })
    .limit(10)

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 500 })
  }

  return NextResponse.json({ users })
}
```

### Row Level Security (RLS)

```typescript
// Supabase automatically enforces RLS based on authenticated user
export async function GET(request: NextRequest) {
  const supabase = createRouteHandlerClient()

  // Only returns rows the authenticated user can access
  const { data: documents } = await supabase
    .from('documents')
    .select('*')

  return NextResponse.json({ documents })
}
```

---

## Caching

### Static Data (ISR)

```typescript
export async function GET() {
  const data = await fetchStaticData()

  return NextResponse.json(
    { data },
    {
      headers: {
        'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400'
      }
    }
  )
}
```

### Opt-out of Caching

```typescript
export const dynamic = 'force-dynamic' // Opt out of caching
export const revalidate = 0 // Disable ISR

export async function GET() {
  // Always fetch fresh data
  return NextResponse.json({ data: [] })
}
```

---

## CORS Configuration

```typescript
// app/api/public/route.ts
export async function GET(request: NextRequest) {
  const response = NextResponse.json({ data: [] })

  // Allow CORS
  response.headers.set('Access-Control-Allow-Origin', '*')
  response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
  response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')

  return response
}

export async function OPTIONS() {
  return new NextResponse(null, {
    status: 204,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type, Authorization'
    }
  })
}
```

---

## Testing API Routes

```typescript
import { describe, it, expect } from 'vitest'
import { GET, POST } from './route'

describe('/api/users', () => {
  it('should return users list', async () => {
    const request = new Request('http://localhost:3000/api/users')
    const response = await GET(request)
    const data = await response.json()

    expect(response.status).toBe(200)
    expect(Array.isArray(data.users)).toBe(true)
  })

  it('should create a new user', async () => {
    const request = new Request('http://localhost:3000/api/users', {
      method: 'POST',
      body: JSON.stringify({ name: 'John', email: 'john@example.com' })
    })
    const response = await POST(request)
    const data = await response.json()

    expect(response.status).toBe(201)
    expect(data.user).toBeDefined()
  })
})
```

---

*API Routes Standards - MORPH-SPEC by Polymorphism Tech*
