Skip to content
GitHub

API Application

The API application (apps/api) is the backend server that handles all business logic, database operations, and external service integrations. Built with Next.js 15 App Router for serverless deployment.

While you can replace this with any TypeScript framework, we chose Next.js because:

  • Automatic scaling - Handle traffic spikes without configuration
  • Edge deployment - Deploy to Vercel, AWS, or any serverless platform
  • Zero cold starts - Optimized for serverless performance
  • Built-in optimizations - Automatic code splitting and bundling
  • TypeScript native - Full type safety out of the box
  • File-based routing - API routes match file structure
  • Built-in middleware - Request/response handling
  • Hot reload - Instant development feedback
  • Automatic HTTPS - SSL certificates handled automatically
  • Built-in monitoring - Error tracking and performance metrics
  • Easy deployment - One command to deploy to production
  • Environment management - Secure environment variable handling

Framework: Next.js 15 App Router (Serverless)
Port: 3002 (development)
Database: Neon PostgreSQL with Drizzle ORM
Authentication: Custom JWT-based authentication
Deployment: Vercel (or any serverless platform)

While we provide a complete Next.js API, you can replace it with any TypeScript framework:

  • Fastify - High-performance alternative to Express
  • Hono - Lightweight, edge-first framework
  • Bun - Ultra-fast JavaScript runtime with built-in HTTP server
  • NestJS - Enterprise-grade framework with decorators
  • Express - Most popular Node.js framework
  • Koa - Modern Express alternative
  • tRPC - End-to-end typesafe APIs
  • GraphQL - Query language with strong typing
  • gRPC - High-performance RPC framework
  • Cloudflare Workers - Edge computing platform
  • Deno Deploy - Deno’s serverless platform
  • Vercel Edge Functions - Edge runtime for Vercel

All maintain the same TypeScript types and monorepo structure!

apps/api/
├── app/
│   ├── auth/
│   │   ├── login/route.ts         # User login endpoint
│   │   ├── logout/route.ts        # User logout endpoint
│   │   ├── me/route.ts            # Get current user
│   │   └── register/route.ts      # User registration endpoint
│   ├── account/
│   │   ├── delete/route.ts        # Delete user account
│   │   └── profile/route.ts       # User profile management
│   ├── billing-portal/route.ts    # Stripe Customer Portal
│   ├── checkout/route.ts          # Stripe Checkout Sessions
│   ├── health/route.ts            # Health check endpoint
│   ├── preferences/route.ts       # User preferences CRUD
│   ├── subscription/route.ts      # Subscription management
│   ├── tasks/[id]/route.ts        # Task CRUD operations
│   ├── tasks/route.ts             # Task list operations
│   └── webhooks/stripe/route.ts   # Stripe webhook handler
├── lib/
│   └── validation.ts              # Zod error formatting
├── middleware.ts                  # Request middleware
└── package.json                   # Dependencies and scripts
POST /auth/register  # Create new user account
POST /auth/login     # Sign in with email/password
GET  /auth/me        # Get current user info
POST /auth/logout    # Sign out (clear cookie)
GET    /account/profile  # Get user profile
PATCH  /account/profile  # Update user profile
DELETE /account/delete   # Delete user account

Example Registration:

// POST /auth/register
const response = await fetch("/api/auth/register", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    name: "John Doe",
    email: "john@example.com",
    password: "securepassword123",
  }),
});

Example Login:

// POST /auth/login
const response = await fetch("/api/auth/login", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    email: "john@example.com",
    password: "securepassword123",
  }),
});

// Returns: { success: true, token: "...", user: {...} }
GET /health

Returns 200 OK for health monitoring.

GET    /tasks           # List user's tasks
POST   /tasks           # Create new task
PATCH  /tasks/[id]      # Update task
DELETE /tasks/[id]      # Delete task

Example Request:

// GET /tasks (uses httpOnly cookie for auth)
const response = await fetch("/api/tasks");

// POST /tasks
const response = await fetch("/api/tasks", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    title: "Complete project",
    description: "Finish the Orion Kit documentation",
    status: "todo",
  }),
});
GET    /preferences     # Get user preferences
PATCH  /preferences     # Update user preferences

Example Response:

{
  "success": true,
  "data": {
    "plan": "pro",
    "defaultStatus": "todo",
    "emailNotifications": true,
    "taskReminders": false,
    "weeklyDigest": true,
    "stripeCustomerId": "cus_1234567890",
    "stripeSubscriptionId": "sub_1234567890",
    "stripeSubscriptionStatus": "active"
  }
}
POST   /checkout        # Create Stripe Checkout Session
POST   /billing-portal  # Create Stripe Customer Portal Session
GET    /subscription    # Get subscription details
DELETE /subscription    # Cancel subscription

Example Checkout:

// POST /checkout
const response = await fetch("/api/checkout", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    priceId: "price_1234567890",
  }),
});

// Returns: { success: true, data: { url: "https://checkout.stripe.com/..." } }
POST   /webhooks/stripe # Stripe webhook handler

Handles Stripe events:

  • checkout.session.completed - New subscription
  • customer.subscription.updated - Plan changes
  • customer.subscription.deleted - Cancellation
  • invoice.payment_succeeded - Successful payment
  • invoice.payment_failed - Failed payment

All protected endpoints require authentication via JWT tokens stored in httpOnly cookies:

import { getCurrentUser } from "@workspace/auth/server";

export async function GET(req: NextRequest) {
  const user = await getCurrentUser(req);

  if (!user) {
    return NextResponse.json(
      { success: false, error: "Unauthorized" },
      { status: 401 }
    );
  }

  // Authenticated logic here - user.id is available
}
  1. Login/Register - User submits credentials
  2. JWT Creation - Server creates signed JWT token
  3. Cookie Storage - Token stored in httpOnly cookie
  4. Automatic Verification - Middleware verifies token on protected routes
  5. User Context - getCurrentUser() extracts user info from token

Uses Drizzle ORM with type-safe queries:

import { db, tasks, userPreferences } from "@workspace/database";
import { eq } from "drizzle-orm";

// Get user's tasks
const userTasks = await db.select().from(tasks).where(eq(tasks.userId, userId));

// Update user preferences
await db
  .update(userPreferences)
  .set({ plan: "pro" })
  .where(eq(userPreferences.userId, userId));

All requests are validated with Zod schemas:

import { createTaskInputSchema } from "@workspace/types";
import { validationErrorResponse } from "@/lib/validation";

export async function POST(request: Request) {
  try {
    const body = await request.json();
    const validated = createTaskInputSchema.parse(body);

    // Process validated data
  } catch (error) {
    if (error instanceof ZodError) {
      return validationErrorResponse(error.errors);
    }
    throw error;
  }
}

Consistent error responses:

// Validation error
{
  "success": false,
  "error": "Validation failed",
  "details": [
    "Title is required - title",
    "Status must be one of: todo, in-progress, completed - status"
  ]
}

// Authentication error
{
  "success": false,
  "error": "Unauthorized"
}

// Server error
{
  "success": false,
  "error": "Internal server error"
}
# apps/api/.env.local
DATABASE_URL=postgresql://...
AUTH_JWT_SECRET=your-super-secret-key-min-32-chars
NEXT_PUBLIC_APP_URL=http://localhost:3001
NEXT_PUBLIC_API_URL=http://localhost:3002
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_AXIOM_TOKEN=xaat-...
NEXT_PUBLIC_AXIOM_DATASET=orion-kit
cd apps/api
pnpm dev

Server runs on http://localhost:3002

# Health check
curl http://localhost:3002/health

# Register user
curl -X POST http://localhost:3002/auth/register \
  -H "Content-Type: application/json" \
  -d '{"name":"Test User","email":"test@example.com","password":"password123"}'

# Login (sets httpOnly cookie)
curl -X POST http://localhost:3002/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"test@example.com","password":"password123"}' \
  -c cookies.txt

# Get tasks (uses cookie for auth)
curl -b cookies.txt http://localhost:3002/tasks
# Open Drizzle Studio
pnpm db:studio

Set production environment variables in Vercel dashboard:

  • AUTH_JWT_SECRET - JWT signing secret (32+ characters)
  • DATABASE_URL - Production database URL
  • NEXT_PUBLIC_APP_URL - Production app URL
  • NEXT_PUBLIC_API_URL - Production API URL
  • STRIPE_SECRET_KEY - Stripe live key
  • STRIPE_WEBHOOK_SECRET - Stripe webhook secret
  • NEXT_PUBLIC_AXIOM_TOKEN - Axiom logging token
  • NEXT_PUBLIC_AXIOM_DATASET - Axiom dataset name

Configure Stripe webhook endpoint:

https://api.orion-kit.dev/webhooks/stripe

Events to listen for:

  • checkout.session.completed
  • customer.subscription.updated
  • customer.subscription.deleted
  • invoice.payment_succeeded
  • invoice.payment_failed

Monitor API health:

# Basic health check
curl https://api.orion-kit.dev/health

# Should return: OK

Logs are sent to Axiom for monitoring:

import { logger } from "@workspace/observability";

logger.info("Task created", { userId, taskId });
logger.error("Payment failed", { userId, error: error.message });

Track API usage with PostHog:

import { track } from "@workspace/analytics";

track("task_created", { userId, taskId });
track("subscription_upgraded", { userId, plan: "pro" });