Skip to content

API Guide ​

This is a single Next.js 16 app — there is no separate API server and no OpenAPI spec. The "API surface" is three things: Server-Component data fetching, Server Actions, and Route Handlers.

SurfaceWhereUse forCross-origin?
Server Component fetchapp/**/page.tsx (async)reading data to render a pageNo
Server Action ("use server")actions/*.tsmutations from forms/componentsNo
Route Handlerapp/api/**/route.tswebhooks, health, OAuthYes

Route Handlers ​

Route Handlers live in next-app/app/api/ and follow the App Router route.ts convention. Use them only when something outside the app must reach an HTTP URL.

Health Check ​

GET /api/health

Returns { status: "ok", timestamp: "..." } from app/api/health/route.ts. Use this to verify the deployed app is reachable and the interactive API playground is pointed at the right base URL.

Current Route Handler Surface ​

GET|POST /api/auth/[...nextauth]      → Auth.js v5 (session, sign-in/out, OAuth callback)
GET      /api/health                  → health probe
POST     /api/billing/stripe/webhook  → Stripe webhook (signature-verified, idempotent)
POST     /api/billing/ecpay/return    → ECPay 綠界 first-auth notification
POST     /api/billing/ecpay/period    → ECPay 綠界 recurring-cycle notification

Auth Session (Auth.js v5) ​

Auth.js v5 uses the JWT strategy — sessions are stateless. The session object is:

ts
type Session = {
  user: {
    id: string
    email: string
    name: string | null
    role: 'viewer' | 'editor' | 'admin'
  }
  expires: string
}

Read it server-side with auth() from @/lib/auth. The Credentials provider requires JWT sessions (it cannot create DrizzleAdapter database sessions).

Server Actions ​

Server Actions ("use server") run on the server and are invoked directly from React components or forms. They compile to same-origin POST endpoints — they cannot be called cross-origin.

Items (actions/items.ts) ​

ts
createItem(prevState, formData): Promise<{ error?: string } | null>
updateItem(id, prevState, formData): Promise<{ error?: string } | null>
deleteItem(id): Promise<{ error?: string } | null>

Each mutation calls requireEditor(), validates input, and scopes the Drizzle WHERE clause by userId. Returning null signals success (the modal closes, the list revalidates).

Account (actions/user.ts) ​

Profile update, password change, account deletion — each derives the user ID from the session, never the request body, so a user can only act on their own account.

Admin (actions/admin.ts) ​

Requires the admin role — requireAdmin() redirects everyone else.

ts
getAllUsers(): Promise<User[]>
setUserRole(userId, role): Promise<{ success: true } | { error: string }>
deleteUser(userId): Promise<{ success: true } | { error: string }>

setUserRole keeps a runtime VALID_ROLES allowlist and refuses self-demotion / self-deletion.

RBAC Guard Pattern ​

Guards live in @/lib/permissions. Each resolves the session and re-reads the role from the database (not the JWT snapshot), so a demotion takes effect immediately.

GuardAllowsRedirects
requireAuth()any signed-in user/login
requireEditor()admin, editor/dashboard
requireAdmin()admin/dashboard
ts
import { requireAdmin } from '@/lib/permissions'

export async function adminAction() {
  'use server'
  const session = await requireAdmin() // re-reads role from DB; redirects non-admins
  // ...admin logic
}

Server Actions are public POST endpoints — never trust an argument's TypeScript type at runtime. Validate against a shared Zod schema (lib/validations/*) or a runtime allowlist.

Validation Envelope ​

Item/account actions return a small state object — { error?: string } | null (null = success). Admin actions return { success: true } | { error: string }. Validation schemas are shared from lib/validations/* so the same Zod rules run on the client form and the server action.

Interactive Playground ​

Test live Route Handlers against your deployed Next.js app:

GET/api/health
Base URL (your deployed Next.js app)
Authorization Header (optional)
Try in Live App →

Released under the MIT License.