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 dev106 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 install2. Configure environment
# 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:dev4. Start API server
pnpm --filter oneapp-api devVisit 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
| Property | Value |
|---|---|
| Location | platform/apps/oneapp-api |
| Port | 3001 |
| Framework | Next.js 16 (App Router) |
| ORM | @repo/oneapp-shared/orm |
| Endpoints | 106 explicit REST routes |
| Domains | 10 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
| Component | Path | Purpose |
|---|---|---|
| Route Handlers | src/app/api/v1/{domain}/{model}/route.ts | Collection endpoints (GET, POST) |
| Item Handlers | src/app/api/v1/{domain}/{model}/[id]/route.ts | Item endpoints (GET, PUT, DELETE) |
| Error Handler | src/lib/error-handler.ts | Maps ORM errors to HTTP responses |
| Middleware | src/middleware.ts | CORS and security headers |
| OpenAPI Generator | scripts/generate-openapi-paths.mjs | Generates 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=0Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
take | number | 20 | Number of records to return |
skip | number | 0 | Number 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
| Code | Meaning | When |
|---|---|---|
| 400 | Bad Request | Validation errors, invalid input |
| 404 | Not Found | Record not found |
| 409 | Conflict | Unique constraint violation |
| 500 | Internal Server Error | Unexpected 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:openapiHow It Works
The generator:
- Scans all
route.tsfiles insrc/app/api/v1/ - Detects HTTP methods (GET, POST, PUT, DELETE)
- Extracts model names from ORM function calls
- Generates OpenAPI path definitions with proper schemas
- 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 SDKPath 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"
});With @repo/oneapp-sdk (Recommended)
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 buildThe 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:watchDeployment
Vercel
// vercel.json
{
"buildCommand": "pnpm build",
"outputDirectory": ".next",
"framework": "nextjs"
}Environment Setup
- Database - Configure Neon Postgres connection strings
- CORS - Configure allowed origins for production
- 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 buildRelated Apps
- oneapp-onstage - Main consumer application
- oneapp-backstage - Admin dashboard
- mobile-app - React Native app (API consumer)
Related Packages
- @repo/oneapp-shared - Database models and ORM
- @repo/db-prisma - Prisma client