OneApp Docs
Platform Apps

oneapp-api

Production REST API server exposing Prisma ORM as REST endpoints. 106 explicit routes across 10 domains. OpenAPI generation, type-safe SDK, versioned endpoints. CORS-enabled, comprehensive error handling.

Quick Start

Run the API in 2 minutes:

pnpm --filter oneapp-api dev

106 REST endpoints, OpenAPI generation included. Skip to Quick Start →

Why oneapp-api?

Database access duplicated across apps. REST endpoints manually coded for each model. No OpenAPI spec for API documentation. SDK generation requires manual type definitions. Error handling inconsistent across routes. CORS configuration scattered. API versioning not standardized.

oneapp-api solves this with explicit REST routes for all 55 Prisma models, automatic OpenAPI generation, and type-safe SDK.

Production-ready with 106 versioned endpoints, automatic OpenAPI paths, comprehensive error handling, CORS support, and full TypeScript client generation.

Use cases

  • REST API access — Expose Prisma database operations as HTTP endpoints
  • SDK generation — Auto-generate TypeScript SDK with full type safety
  • Mobile app backend — Power React Native mobile app with REST API
  • Third-party integrations — Provide REST API for external services
  • Microservice architecture — Separate data access layer from frontend

How it works

oneapp-api exposes ORM functions as explicit REST routes:

// src/app/api/v1/crm/contact/route.ts
import { findManyContactsOrm, createContactOrm } from "@repo/oneapp-shared/orm/crm";

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const take = Number.parseInt(searchParams.get("take") ?? "20", 10);
  const skip = Number.parseInt(searchParams.get("skip") ?? "0", 10);

  const result = await findManyContactsOrm(undefined, undefined, undefined, {
    take,
    skip
  });
  return Response.json(result);
}

export async function POST(request: NextRequest) {
  const data = await request.json();
  const result = await createContactOrm(data);
  return Response.json(result, { status: 201 });
}

Uses Next.js 16 App Router for routing, @repo/oneapp-shared ORM for database access, auto-generates OpenAPI paths, and powers @repo/oneapp-sdk TypeScript client.

Key features

106 REST endpoints — Explicit routes for all 55 Prisma models

OpenAPI generation — Auto-generates paths for SDK generation

Type-safe SDK — Powers @repo/oneapp-sdk TypeScript client

API versioning — All endpoints under /api/v1/ for future compatibility

Domain organization — Routes organized by Prisma schema (ai, crm, auth)

Error handling — ORM errors mapped to appropriate HTTP status codes

Quick Start

1. Install dependencies

pnpm install

2. Configure environment

.env.local
# Database (inherited from @repo/oneapp-shared)
DATABASE_URL="postgresql://user:pass@host/db?sslmode=require"
DATABASE_URL_UNPOOLED="postgresql://user:pass@host/db?sslmode=require"

3. Run database migrations

pnpm --filter @repo/oneapp-shared prisma:migrate:dev

4. Start API server

pnpm --filter oneapp-api dev

Visit http://localhost:3001/api/v1/crm/contact to test an endpoint.

TypeScript SDK

Use the auto-generated SDK for full type safety:

import { listContacts, createContact } from "@repo/oneapp-sdk";

const { data } = await listContacts({ query: { take: 10 } });

Technical Details

For Developers: Technical implementation details

Overview

PropertyValue
Locationplatform/apps/oneapp-api
Port3001
FrameworkNext.js 16 (App Router)
ORM@repo/oneapp-shared/orm
Endpoints106 explicit REST routes
Domains10 Prisma schema domains

Architecture

Route Structure

All API routes organized by Prisma schema domains:

src/app/api/v1/
├── ai/                     # ai.prisma models (11 models)
│   ├── conversation/       # AIConversation
│   │   ├── route.ts        # GET (list), POST (create)
│   │   └── [id]/
│   │       └── route.ts    # GET, PUT, DELETE by ID
│   ├── message/            # AIMessage
│   ├── artifact/           # AIArtifact
│   ├── vote/               # AIVote
│   ├── suggestion/         # AISuggestion
│   ├── stream/             # AIStream
│   ├── generation-step/    # AIGenerationStep
│   ├── chain/              # AIFallbackChain
│   ├── chain-provider/     # AIFallbackChainProvider
│   ├── attempt/            # AIProviderAttempt
│   └── route.ts            # AIProvider
├── ai-agent/              # ai-agent.prisma models (5 models)
│   ├── config/            # AgentConfig
│   ├── message/           # AgentMessage
│   ├── deliverable/       # AgentDeliverable
│   ├── evaluation-result/ # AgentEvaluationResult
│   └── optimization-result/ # AgentOptimizationResult
├── ai-rag/                # ai-rag.prisma models (7 models)
│   ├── embedding/         # RAGEmbedding
│   ├── document/          # RAGDocument
│   ├── chunk/             # RAGChunk
│   ├── query/             # RAGQuery
│   ├── result/            # RAGResult
│   ├── index/             # RAGIndex
│   └── metadata/          # RAGMetadata
├── ai-research/           # ai-research.prisma models (6 models)
├── analytics/             # analytics.prisma models (4 models)
├── auth/                  # auth.prisma models (13 models)
│   ├── user/              # User
│   ├── session/           # Session
│   ├── account/           # Account
│   ├── verification/      # Verification
│   ├── two-factor/        # TwoFactor
│   ├── passkey/           # Passkey
│   ├── api-key/           # APIKey
│   ├── api-key-analytics/ # APIKeyAnalytics
│   ├── organization/      # Organization
│   ├── member/            # Member
│   ├── invitation/        # Invitation
│   ├── team/              # Team
│   └── team-member/       # TeamMember
├── crm/                   # oneapp-crm.prisma models (2 models)
│   ├── contact/           # Contact
│   └── deal/              # Deal
└── storage/               # storage.prisma models (3 models)

Key Components

ComponentPathPurpose
Route Handlerssrc/app/api/v1/{domain}/{model}/route.tsCollection endpoints (GET, POST)
Item Handlerssrc/app/api/v1/{domain}/{model}/[id]/route.tsItem endpoints (GET, PUT, DELETE)
Error Handlersrc/lib/error-handler.tsMaps ORM errors to HTTP responses
Middlewaresrc/middleware.tsCORS and security headers
OpenAPI Generatorscripts/generate-openapi-paths.mjsGenerates OpenAPI paths for SDK

API Endpoints

Endpoint Format

/api/v1/{domain}/{model}
/api/v1/{domain}/{model}/{id}

Where:

  • v1 = API version
  • {domain} = Prisma schema name (ai, crm, auth, etc.)
  • {model} = Model name in kebab-case
  • {id} = Resource ID (for item operations)

Collection Operations

List Resources

GET /api/v1/{domain}/{model}?take=20&skip=0

Query Parameters:

ParameterTypeDefaultDescription
takenumber20Number of records to return
skipnumber0Number of records to skip

Example:

curl "http://localhost:3001/api/v1/crm/contact?take=10&skip=0"

Response:

{
  "data": [
    {
      "id": "contact-1",
      "firstName": "John",
      "lastName": "Doe",
      "email": "john@example.com"
    }
  ],
  "meta": {
    "count": 10,
    "take": 10,
    "skip": 0
  }
}

Create Resource

POST /api/v1/{domain}/{model}

Body: JSON object matching the model schema

Example:

curl -X POST "http://localhost:3001/api/v1/crm/contact" \
  -H "Content-Type: application/json" \
  -d '{
    "firstName": "Alice",
    "lastName": "Johnson",
    "email": "alice@example.com"
  }'

Response:

{
  "id": "contact-3",
  "firstName": "Alice",
  "lastName": "Johnson",
  "email": "alice@example.com",
  "createdAt": "2024-12-15T10:30:00.000Z"
}

Resource Operations

Get Resource

GET /api/v1/{domain}/{model}/{id}

Example:

curl "http://localhost:3001/api/v1/crm/contact/contact-1"

Response:

{
  "id": "contact-1",
  "firstName": "John",
  "lastName": "Doe",
  "email": "john@example.com",
  "createdAt": "2024-12-10T08:00:00.000Z",
  "updatedAt": "2024-12-15T10:00:00.000Z"
}

Update Resource

PUT /api/v1/{domain}/{model}/{id}

Example:

curl -X PUT "http://localhost:3001/api/v1/crm/contact/contact-1" \
  -H "Content-Type: application/json" \
  -d '{"email": "newemail@example.com"}'

Delete Resource

DELETE /api/v1/{domain}/{model}/{id}

Example:

curl -X DELETE "http://localhost:3001/api/v1/crm/contact/contact-1"

Response:

{
  "id": "contact-1",
  "deleted": true
}

Available Domains

AI Domain (/api/v1/ai/)

11 models for AI conversations, messages, providers, and artifacts:

  • /api/v1/ai/conversation - AI conversations
  • /api/v1/ai/message - AI messages
  • /api/v1/ai/artifact - AI artifacts
  • /api/v1/ai/vote - AI votes
  • /api/v1/ai/suggestion - AI suggestions
  • /api/v1/ai/stream - AI streams
  • /api/v1/ai/generation-step - Generation steps
  • /api/v1/ai/chain - Fallback chains
  • /api/v1/ai/chain-provider - Fallback chain providers
  • /api/v1/ai/attempt - Provider attempts
  • /api/v1/ai - AI providers

CRM Domain (/api/v1/crm/)

2 models for customer relationship management:

  • /api/v1/crm/contact - CRM contacts
  • /api/v1/crm/deal - CRM deals

Auth Domain (/api/v1/auth/)

13 models for authentication and authorization:

  • /api/v1/auth/user - Users
  • /api/v1/auth/session - Sessions
  • /api/v1/auth/account - OAuth accounts
  • /api/v1/auth/verification - Email verifications
  • /api/v1/auth/two-factor - Two-factor auth
  • /api/v1/auth/passkey - Passkeys
  • /api/v1/auth/api-key - API keys
  • /api/v1/auth/api-key-analytics - API key usage
  • /api/v1/auth/organization - Organizations
  • /api/v1/auth/member - Organization members
  • /api/v1/auth/invitation - Invitations
  • /api/v1/auth/team - Teams
  • /api/v1/auth/team-member - Team members

AI Agent Domain (/api/v1/ai-agent/)

5 models for agent configurations and results:

  • /api/v1/ai-agent/config - Agent configurations
  • /api/v1/ai-agent/message - Agent messages
  • /api/v1/ai-agent/deliverable - Agent deliverables
  • /api/v1/ai-agent/evaluation-result - Evaluation results
  • /api/v1/ai-agent/optimization-result - Optimization results

AI RAG Domain (/api/v1/ai-rag/)

7 models for retrieval-augmented generation:

  • /api/v1/ai-rag/embedding - Vector embeddings
  • /api/v1/ai-rag/document - RAG documents
  • /api/v1/ai-rag/chunk - Document chunks
  • /api/v1/ai-rag/query - RAG queries
  • /api/v1/ai-rag/result - Query results
  • /api/v1/ai-rag/index - Vector indexes
  • /api/v1/ai-rag/metadata - Document metadata

AI Research Domain (/api/v1/ai-research/)

6 models for research capabilities

Analytics Domain (/api/v1/analytics/)

4 models for analytics and tracking

Storage Domain (/api/v1/storage/)

3 models for file storage

Error Handling

Error Response Format

All errors follow this consistent format:

{
  "error": "Human-readable error message",
  "code": "ERROR_CODE",
  "details": {}
}

HTTP Status Codes

CodeMeaningWhen
400Bad RequestValidation errors, invalid input
404Not FoundRecord not found
409ConflictUnique constraint violation
500Internal Server ErrorUnexpected errors

ORM Error Mapping

// src/lib/error-handler.ts
import { OrmErrorCode } from "@repo/oneapp-shared/orm/errors";

export function createErrorResponse(error: unknown): Response {
  if (error instanceof OrmError) {
    switch (error.code) {
      case OrmErrorCode.UNIQUE_VIOLATION:
        return Response.json({ error: error.message }, { status: 409 });

      case OrmErrorCode.NOT_FOUND:
        return Response.json({ error: "Resource not found" }, { status: 404 });

      case OrmErrorCode.FK_VIOLATION:
      case OrmErrorCode.INVALID_PAGINATION:
      case OrmErrorCode.VALIDATION_ERROR:
        return Response.json({ error: error.message }, { status: 400 });

      default:
        return Response.json({ error: "Internal server error" }, { status: 500 });
    }
  }

  return Response.json({ error: "Unknown error" }, { status: 500 });
}

OpenAPI Generation

Auto-Generate Paths

# Generate OpenAPI paths for all 106 endpoints
pnpm --filter oneapp-api generate:openapi

How It Works

The generator:

  1. Scans all route.ts files in src/app/api/v1/
  2. Detects HTTP methods (GET, POST, PUT, DELETE)
  3. Extracts model names from ORM function calls
  4. Generates OpenAPI path definitions with proper schemas
  5. Outputs to generated/openapi-paths.json

Flow Diagram

Route Files → Scanner → Extract Models → Generate Paths → openapi-paths.json
     ↓                                                              ↓
ORM Imports                                            Merged with Prisma Schemas

                                                        Complete OpenAPI Spec

                                                            HeyAPI Generator

                                                          TypeScript SDK

Path Generation Example

// Input: src/app/api/v1/crm/company/route.ts
import {
  findManyCompaniesOrm,
  createCompanyOrm,
} from "@repo/oneapp-shared/orm/crm";

export async function GET(request: NextRequest) {
  const result = await findManyCompaniesOrm(/* ... */);
  return Response.json(result);
}

export async function POST(request: NextRequest) {
  const data = await request.json();
  const result = await createCompanyOrm(data);
  return Response.json(result, { status: 201 });
}

// Output: generated/openapi-paths.json
{
  "/api/v1/crm/company": {
    "get": {
      "operationId": "listCompanies",
      "summary": "List companies",
      "parameters": [
        { "name": "take", "in": "query", "schema": { "type": "integer" } },
        { "name": "skip", "in": "query", "schema": { "type": "integer" } }
      ],
      "responses": {
        "200": {
          "description": "Successful response",
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CompanyListResponse" }
            }
          }
        }
      }
    },
    "post": {
      "operationId": "createCompany",
      "summary": "Create company",
      "requestBody": {
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/CompanyInput" }
          }
        }
      },
      "responses": {
        "201": {
          "description": "Created",
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/Company" }
            }
          }
        }
      }
    }
  }
}

Using the API

With fetch

// List contacts
const response = await fetch("http://localhost:3001/api/v1/crm/contact?take=10");
const { data, meta } = await response.json();

console.log(data); // Contact[]
console.log(meta); // { count: 10, take: 10, skip: 0 }

// Create contact
const newContact = await fetch("http://localhost:3001/api/v1/crm/contact", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    firstName: "Alice",
    lastName: "Johnson",
    email: "alice@example.com"
  })
}).then((r) => r.json());

console.log(newContact); // { id: "...", firstName: "Alice", ... }

// Update contact
await fetch("http://localhost:3001/api/v1/crm/contact/contact-1", {
  method: "PUT",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    email: "newemail@example.com"
  })
});

// Delete contact
await fetch("http://localhost:3001/api/v1/crm/contact/contact-1", {
  method: "DELETE"
});
import {
  listCompanies,
  createCompany,
  getCompanyById,
  updateCompany,
  deleteCompany,
  type Company
} from "@repo/oneapp-sdk";

// Full type safety - result.data is Company[]
const result = await listCompanies({
  query: { take: 10, skip: 0 }
});

result.data.forEach((company: Company) => {
  console.log(company.name); // Type-safe property access
});

// Create with validation
const newCompany = await createCompany({
  body: {
    name: "Acme Inc",
    industry: "Technology" // Type-checked against schema
  }
});

// Get single
const company = await getCompanyById({
  path: { id: "company-123" }
});

// Update
await updateCompany({
  path: { id: "company-123" },
  body: { name: "Acme Corporation" }
});

// Delete
await deleteCompany({
  path: { id: "company-123" }
});

Adding New Routes

When new models are added to @repo/oneapp-shared:

1. Create Collection Route

// src/app/api/v1/{domain}/{model}/route.ts
import {
  findMany{Model}Orm,
  create{Model}Orm,
} from "@repo/oneapp-shared/orm/{domain}";

import { createErrorResponse } from "#/lib/error-handler";
import type { NextRequest } from "next/server";

export async function GET(request: NextRequest) {
  try {
    const { searchParams } = new URL(request.url);
    const take = Number.parseInt(searchParams.get("take") ?? "20", 10);
    const skip = Number.parseInt(searchParams.get("skip") ?? "0", 10);

    const result = await findMany{Model}Orm(
      undefined,
      undefined,
      undefined,
      { take, skip }
    );

    return Response.json(result);
  } catch (error) {
    return createErrorResponse(error);
  }
}

export async function POST(request: NextRequest) {
  try {
    const data = await request.json();
    const result = await create{Model}Orm(data);
    return Response.json(result, { status: 201 });
  } catch (error) {
    return createErrorResponse(error);
  }
}

2. Create Item Route

// src/app/api/v1/{domain}/{model}/[id]/route.ts
import {
  findUnique{Model}Orm,
  update{Model}Orm,
  delete{Model}Orm,
} from "@repo/oneapp-shared/orm/{domain}";

import { createErrorResponse } from "#/lib/error-handler";
import type { NextRequest } from "next/server";

export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  try {
    const result = await findUnique{Model}Orm({ id: params.id });
    return Response.json(result);
  } catch (error) {
    return createErrorResponse(error);
  }
}

export async function PUT(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  try {
    const data = await request.json();
    const result = await update{Model}Orm({ id: params.id }, data);
    return Response.json(result);
  } catch (error) {
    return createErrorResponse(error);
  }
}

export async function DELETE(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  try {
    const result = await delete{Model}Orm({ id: params.id });
    return Response.json(result);
  } catch (error) {
    return createErrorResponse(error);
  }
}

3. Regenerate OpenAPI & SDK

# Generate OpenAPI paths
pnpm --filter oneapp-api generate:openapi

# Rebuild SDK
pnpm --filter @repo/oneapp-sdk build

The new routes will automatically be:

  • ✅ Included in the OpenAPI spec
  • ✅ Available in the TypeScript SDK
  • ✅ Fully type-safe with generated types

CORS Configuration

// src/middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  // Only apply CORS to /api/v1/* routes
  if (request.nextUrl.pathname.startsWith("/api/v1/")) {
    const response = NextResponse.next();

    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;
  }

  return NextResponse.next();
}

export const config = {
  matcher: "/api/v1/:path*"
};

Testing

Endpoint Tests

import { describe, it, expect } from "vitest";

describe("CRM Contact API", () => {
  it("lists contacts", async () => {
    const response = await fetch("http://localhost:3001/api/v1/crm/contact?take=10");

    expect(response.status).toBe(200);

    const { data, meta } = await response.json();
    expect(Array.isArray(data)).toBe(true);
    expect(meta).toHaveProperty("count");
  });

  it("creates contact", async () => {
    const response = await fetch("http://localhost:3001/api/v1/crm/contact", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        firstName: "Test",
        lastName: "User",
        email: "test@example.com"
      })
    });

    expect(response.status).toBe(201);

    const contact = await response.json();
    expect(contact).toHaveProperty("id");
    expect(contact.firstName).toBe("Test");
  });

  it("handles not found", async () => {
    const response = await fetch("http://localhost:3001/api/v1/crm/contact/nonexistent");

    expect(response.status).toBe(404);

    const { error } = await response.json();
    expect(error).toBeTruthy();
  });
});

Run Tests

# All tests
pnpm --filter oneapp-api test

# Endpoint tests specifically
pnpm --filter oneapp-api test:endpoints

# Watch mode
pnpm --filter oneapp-api test:watch

Deployment

Vercel

// vercel.json
{
  "buildCommand": "pnpm build",
  "outputDirectory": ".next",
  "framework": "nextjs"
}

Environment Setup

  1. Database - Configure Neon Postgres connection strings
  2. CORS - Configure allowed origins for production
  3. Rate limiting - Add rate limiting middleware if needed

Build Command

# Install dependencies
pnpm install

# Run database migrations
pnpm --filter @repo/oneapp-shared prisma:migrate:deploy

# Generate OpenAPI paths
pnpm --filter oneapp-api generate:openapi

# Build API
pnpm --filter oneapp-api build

On this page