OneApp Docs
Guides

API SDK Integration

Build type-safe APIs with automatic client generation — define your data models once in Prisma, get generated ORM functions, REST API endpoints, OpenAPI specs, and TypeScript SDK clients, all with end-to-end type safety.

Know your API setup?

Why API SDK integration matters

Without proper API-SDK integration:

  • Manual API clients — Write fetch() calls by hand, no autocomplete, copy-paste URLs
  • Type mismatches — Server returns User, client expects UserResponse, bugs in production
  • Outdated docs — OpenAPI spec doesn't match actual endpoints, SDK drifts from API
  • Duplicate validation — Write Zod schemas on server AND client for same types
  • Breaking changes — Rename field on server, client code breaks at runtime not compile time
  • HTTP boilerplate — Every component repeats error handling, loading states, headers

API+SDK integration with code generation — Single source of truth (Prisma schema), auto-generated ORM functions with AsyncResult pattern, REST API routes using ORM, OpenAPI spec generated from routes, TypeScript SDK generated from spec, full type safety from database to client — ensures zero manual sync work and compile-time safety.

Production-ready with oneapp-api (263 auto-generated endpoints), @repo/oneapp-sdk (type-safe TypeScript client), @repo/oneapp-shared (35+ Prisma models, 286 OpenAPI types), automatic regeneration on schema changes, and AsyncResult pattern for type-safe error handling.

Use cases

Use API+SDK integration to:

  • Build type-safe apps — Client code knows exact API response shape, TypeScript catches errors
  • Ship faster — No manual API client code, no writing fetch() calls
  • Prevent runtime errors — Breaking changes caught at compile time, not in production
  • Maintain consistency — Single Prisma schema generates everything, no drift
  • Onboard quickly — New developers get autocomplete for all API functions
  • Version APIs safely — TypeScript errors when upgrading SDK if API changed

Quick Start

Essential steps

Set up type-safe API integration in 3 steps:

1. Define Prisma model and generate ORM

// platform/packages/oneapp-shared/prisma/schemas/oneapp-crm.prisma
model Company {
  id        String   @id @default(cuid())
  name      String
  industry  String?
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}
# Generate ORM functions and OpenAPI schemas
pnpm --filter=@repo/oneapp-shared prisma:generate

2. Create API route using generated ORM

// platform/apps/oneapp-api/src/app/api/v1/crm/company/route.ts
import { findManyCompaniesOrm } from "@repo/oneapp-shared/orm/crm";
import type { NextRequest } from "next/server";

export async function GET(request: NextRequest): Promise<Response> {
  const result = await findManyCompaniesOrm();

  if (!result.success) {
    return Response.json({ error: result.error.message }, { status: 500 });
  }

  return Response.json(result.data);
}
# Generate OpenAPI paths from routes
pnpm --filter=oneapp-api generate:openapi

# Build TypeScript SDK
pnpm --filter=@repo/oneapp-sdk build

3. Use generated SDK in your app

// In any client component
import { listCompanies } from "@repo/oneapp-sdk";

const result = await listCompanies({ query: { take: 10 } });
// TypeScript knows exact response shape!

That's it! You have end-to-end type safety from database to client.

How it works

Complete generation pipeline

graph TB
    A[Prisma Schema] -->|prisma generate| B[Prisma Client ORM]
    A -->|prisma-generator-openapi| C[OpenAPI Schemas]
    B --> D[ORM Functions]
    D --> E[API Routes]
    E -->|route scanner| F[OpenAPI Paths]
    C --> G[Merge Schemas + Paths]
    F --> G
    G --> H[Complete OpenAPI Spec]
    H -->|@hey-api/openapi-ts| I[TypeScript SDK]
    I --> J[Your App]
    J -->|HTTP requests| E
    E -->|SQL queries| K[(Database)]

    style G fill:#9f9,stroke:#333
    style I fill:#9f9,stroke:#333
    style H fill:#ff9,stroke:#333

Architecture layers

5 layers of type safety:

  1. Database — PostgreSQL with Prisma schema (source of truth)
  2. ORM — Generated functions with AsyncResult<T> return types
  3. API — Next.js 16 routes using ORM functions
  4. OpenAPI — Auto-generated spec describing all endpoints
  5. SDK — Type-safe client functions matching OpenAPI spec

Flow example:

// 1. Define in Prisma
model User { id String @id, name String }

// 2. Generated ORM function
async function findManyUsersOrm(): Promise<AsyncResult<User[]>>

// 3. API route uses ORM
GET /api/v1/usersfindManyUsersOrm()

// 4. OpenAPI describes it
paths:
  /api/v1/users:
    get:
      responses:
        200:
          schema:
            $ref: '#/components/schemas/User'

// 5. SDK client generated
const listUsers: () => Promise<{ data: User[] }>

// 6. Your code is fully typed!
const users = await listUsers();
users.data[0].name // ✅ TypeScript knows this exists
users.data[0].foo  // ❌ Error: Property 'foo' does not exist

Layer 1: Prisma schema

Define data models

Location: platform/packages/oneapp-shared/prisma/schemas/

Multiple schema files:

oneapp-shared/prisma/schemas/
├── oneapp-crm.prisma       # CRM models (Company, Contact, Deal)
├── oneapp-email.prisma     # Email models (EmailAccount, Message)
├── oneapp-calendar.prisma  # Calendar models (Event, Calendar)
└── oneapp-tasks.prisma     # Task models (Task, Project)

Example schema:

// oneapp-crm.prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
  output   = "../generated/client"
}

generator openapi {
  provider = "prisma-generator-openapi"
  output   = "../generated/openapi"
}

model Company {
  id          String   @id @default(cuid())
  name        String
  industry    String?
  website     String?
  revenue     Int?     // in cents
  employees   Int?
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt

  // Relations
  contacts    Contact[]
  deals       Deal[]

  @@index([name])
}

model Contact {
  id         String   @id @default(cuid())
  firstName  String
  lastName   String
  email      String   @unique
  phone      String?
  title      String?
  companyId  String
  createdAt  DateTime @default(now())
  updatedAt  DateTime @updatedAt

  // Relations
  company    Company  @relation(fields: [companyId], references: [id])
  deals      Deal[]

  @@index([email])
  @@index([companyId])
}

Generate:

pnpm --filter=@repo/oneapp-shared prisma:generate

This creates:

  1. Prisma Clientgenerated/client/ (ORM)
  2. OpenAPI Schemasgenerated/openapi/ (286 types)
  3. TypeScript types — Company, Contact, Deal, etc.

That's it! You now have type-safe database access.

Layer 2: ORM functions

Generated ORM with AsyncResult

Location: platform/packages/oneapp-shared/orm/

Auto-generated functions for each model:

// @repo/oneapp-shared/orm/crm/company.ts
import { getPrismaClient } from "../../prisma/client";
import type { Company, CompanyWhereInput, CompanyOrderByInput, CompanyInclude } from "../../generated/client";
import type { AsyncResult } from "@repo/types";

/**
 * Find many companies with pagination.
 */
export async function findManyCompaniesOrm(
  where?: CompanyWhereInput,
  orderBy?: CompanyOrderByInput,
  include?: CompanyInclude,
  pagination?: { take?: number; skip?: number }
): Promise<
  AsyncResult<{
    data: Company[];
    total: number;
    page: number;
    pageSize: number;
    hasMore: boolean;
  }>
> {
  try {
    const prisma = getPrismaClient();
    const take = pagination?.take ?? 20;
    const skip = pagination?.skip ?? 0;

    const [data, total] = await Promise.all([
      prisma.company.findMany({
        where,
        orderBy: orderBy ?? { createdAt: "desc" },
        include,
        take,
        skip
      }),
      prisma.company.count({ where })
    ]);

    return {
      success: true,
      data: {
        data,
        total,
        page: Math.floor(skip / take),
        pageSize: take,
        hasMore: total > skip + data.length
      }
    };
  } catch (error) {
    return {
      success: false,
      error: error instanceof Error ? error : new Error("Unknown error")
    };
  }
}

/**
 * Create a new company.
 */
export async function createCompanyOrm(data: {
  name: string;
  industry?: string;
  website?: string;
  revenue?: number;
  employees?: number;
}): Promise<AsyncResult<Company>> {
  try {
    const prisma = getPrismaClient();

    const company = await prisma.company.create({
      data: {
        name: data.name,
        industry: data.industry,
        website: data.website,
        revenue: data.revenue,
        employees: data.employees
      }
    });

    return { success: true, data: company };
  } catch (error) {
    return {
      success: false,
      error: error instanceof Error ? error : new Error("Failed to create company")
    };
  }
}

/**
 * Find a single company by ID.
 */
export async function findUniqueCompanyOrm(id: string, include?: CompanyInclude): Promise<AsyncResult<Company | null>> {
  try {
    const prisma = getPrismaClient();

    const company = await prisma.company.findUnique({
      where: { id },
      include
    });

    return { success: true, data: company };
  } catch (error) {
    return {
      success: false,
      error: error instanceof Error ? error : new Error("Failed to find company")
    };
  }
}

/**
 * Update a company.
 */
export async function updateCompanyOrm(
  id: string,
  data: { name?: string; industry?: string; website?: string; revenue?: number; employees?: number }
): Promise<AsyncResult<Company>> {
  try {
    const prisma = getPrismaClient();

    const company = await prisma.company.update({
      where: { id },
      data
    });

    return { success: true, data: company };
  } catch (error) {
    return {
      success: false,
      error: error instanceof Error ? error : new Error("Failed to update company")
    };
  }
}

/**
 * Delete a company.
 */
export async function deleteCompanyOrm(id: string): Promise<AsyncResult<Company>> {
  try {
    const prisma = getPrismaClient();

    const company = await prisma.company.delete({
      where: { id }
    });

    return { success: true, data: company };
  } catch (error) {
    return {
      success: false,
      error: error instanceof Error ? error : new Error("Failed to delete company")
    };
  }
}

Usage in server code:

// Server component or API route
import { findManyCompaniesOrm, createCompanyOrm } from "@repo/oneapp-shared/orm/crm";

// List companies
const result = await findManyCompaniesOrm(
  { industry: "Technology" }, // where
  { name: "asc" }, // orderBy
  { contacts: true }, // include
  { take: 10, skip: 0 } // pagination
);

if (!result.success) {
  console.error(result.error);
  return;
}

console.log(result.data.data); // Company[]
console.log(result.data.total); // number

That's it! Type-safe database operations with AsyncResult pattern.

Layer 3: API routes

Create REST endpoints using ORM

Location: platform/apps/oneapp-api/src/app/api/v1/

Directory structure matches URL structure:

oneapp-api/src/app/api/v1/
├── crm/
│   ├── company/
│   │   ├── route.ts              # GET /api/v1/crm/company, POST
│   │   └── [id]/
│   │       └── route.ts          # GET /api/v1/crm/company/:id, PATCH, DELETE
│   ├── contact/
│   │   ├── route.ts
│   │   └── [id]/route.ts
│   └── deal/
│       ├── route.ts
│       └── [id]/route.ts
├── email/
│   └── account/
│       └── route.ts
└── calendar/
    └── event/
        └── route.ts

Example API route:

// platform/apps/oneapp-api/src/app/api/v1/crm/company/route.ts
import { findManyCompaniesOrm, createCompanyOrm } from "@repo/oneapp-shared/orm/crm";
import type { NextRequest } from "next/server";
import { z } from "zod";

// GET /api/v1/crm/company
export async function GET(request: NextRequest): Promise<Response> {
  try {
    const { searchParams } = new URL(request.url);

    // Parse query params
    const take = Number.parseInt(searchParams.get("take") ?? "20", 10);
    const skip = Number.parseInt(searchParams.get("skip") ?? "0", 10);
    const industry = searchParams.get("industry") ?? undefined;

    // Call ORM function
    const result = await findManyCompaniesOrm(industry ? { industry } : undefined, { createdAt: "desc" }, undefined, {
      take,
      skip
    });

    // Handle result
    if (!result.success) {
      return Response.json({ error: result.error.message }, { status: 500 });
    }

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

// POST /api/v1/crm/company
export async function POST(request: NextRequest): Promise<Response> {
  try {
    const body = await request.json();

    // Validate input
    const schema = z.object({
      name: z.string().min(1),
      industry: z.string().optional(),
      website: z.string().url().optional(),
      revenue: z.number().int().positive().optional(),
      employees: z.number().int().positive().optional()
    });

    const validated = schema.parse(body);

    // Create company
    const result = await createCompanyOrm(validated);

    if (!result.success) {
      return Response.json({ error: result.error.message }, { status: 400 });
    }

    return Response.json(result.data, { status: 201 });
  } catch (error) {
    if (error instanceof z.ZodError) {
      return Response.json({ error: "Validation error", details: error.errors }, { status: 400 });
    }

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

Dynamic route for single company:

// platform/apps/oneapp-api/src/app/api/v1/crm/company/[id]/route.ts
import { findUniqueCompanyOrm, updateCompanyOrm, deleteCompanyOrm } from "@repo/oneapp-shared/orm/crm";
import type { NextRequest } from "next/server";
import { z } from "zod";

// GET /api/v1/crm/company/:id
export async function GET(request: NextRequest, { params }: { params: { id: string } }): Promise<Response> {
  try {
    const result = await findUniqueCompanyOrm(params.id);

    if (!result.success) {
      return Response.json({ error: result.error.message }, { status: 500 });
    }

    if (!result.data) {
      return Response.json({ error: "Company not found" }, { status: 404 });
    }

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

// PATCH /api/v1/crm/company/:id
export async function PATCH(request: NextRequest, { params }: { params: { id: string } }): Promise<Response> {
  try {
    const body = await request.json();

    // Validate input
    const schema = z.object({
      name: z.string().min(1).optional(),
      industry: z.string().optional(),
      website: z.string().url().optional(),
      revenue: z.number().int().positive().optional(),
      employees: z.number().int().positive().optional()
    });

    const validated = schema.parse(body);

    // Update company
    const result = await updateCompanyOrm(params.id, validated);

    if (!result.success) {
      return Response.json({ error: result.error.message }, { status: 400 });
    }

    return Response.json(result.data);
  } catch (error) {
    if (error instanceof z.ZodError) {
      return Response.json({ error: "Validation error", details: error.errors }, { status: 400 });
    }

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

// DELETE /api/v1/crm/company/:id
export async function DELETE(request: NextRequest, { params }: { params: { id: string } }): Promise<Response> {
  try {
    const result = await deleteCompanyOrm(params.id);

    if (!result.success) {
      return Response.json({ error: result.error.message }, { status: 400 });
    }

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

Start API server:

pnpm --filter=oneapp-api dev
# Server running on http://localhost:3001

Test endpoint:

curl http://localhost:3001/api/v1/crm/company?take=5

That's it! REST API using type-safe ORM functions.

Layer 4: OpenAPI generation

Auto-generate OpenAPI spec from routes

Generate paths from API routes:

pnpm --filter=oneapp-api generate:openapi

This scans all route.ts files and:

  1. Detects HTTP methods (GET, POST, PATCH, DELETE)
  2. Extracts route patterns (/api/v1/crm/company)
  3. Identifies ORM functions used (findManyCompaniesOrm → Company model)
  4. Generates OpenAPI path definitions

Generated paths:

# platform/apps/oneapp-api/generated/openapi-paths.json
{
  "paths":
    {
      "/api/v1/crm/company":
        {
          "get":
            {
              "operationId": "listCompanies",
              "summary": "List companies",
              "parameters":
                [
                  { "name": "take", "in": "query", "schema": { "type": "integer", "default": 20 } },
                  { "name": "skip", "in": "query", "schema": { "type": "integer", "default": 0 } },
                  { "name": "industry", "in": "query", "schema": { "type": "string" } }
                ],
              "responses":
                {
                  "200":
                    {
                      "description": "Success",
                      "content":
                        {
                          "application/json":
                            {
                              "schema":
                                {
                                  "type": "object",
                                  "properties":
                                    {
                                      "data": { "type": "array", "items": { "$ref": "#/components/schemas/Company" } },
                                      "total": { "type": "integer" },
                                      "page": { "type": "integer" },
                                      "pageSize": { "type": "integer" },
                                      "hasMore": { "type": "boolean" }
                                    }
                                }
                            }
                        }
                    }
                }
            },
          "post":
            {
              "operationId": "createCompany",
              "summary": "Create company",
              "requestBody":
                {
                  "required": true,
                  "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CompanyCreateInput" } } }
                },
              "responses":
                {
                  "201":
                    {
                      "description": "Created",
                      "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Company" } } }
                    }
                }
            }
        },
      "/api/v1/crm/company/{id}":
        {
          "get":
            {
              "operationId": "getCompany",
              "summary": "Get company by ID",
              "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
              "responses":
                {
                  "200":
                    {
                      "description": "Success",
                      "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Company" } } }
                    },
                  "404": { "description": "Not found" }
                }
            }
        }
    }
}

Merge with Prisma-generated schemas:

# SDK build merges:
# 1. platform/packages/oneapp-shared/generated/openapi/ (schemas from Prisma)
# 2. platform/apps/oneapp-api/generated/openapi-paths.json (paths from routes)
# → Complete OpenAPI spec

That's it! OpenAPI spec automatically generated from your code.

Layer 5: TypeScript SDK generation

Generate type-safe client from OpenAPI

Build SDK:

pnpm --filter=@repo/oneapp-sdk build

This:

  1. Merges schemas + paths into complete OpenAPI spec
  2. Runs @hey-api/openapi-ts to generate TypeScript client
  3. Creates 263 type-safe functions

Generated SDK functions:

// platform/packages/oneapp-sdk/generated/sdk/services.gen.ts

/**
 * List companies
 */
export const listCompanies = <ThrowOnError extends boolean = false>(
  options?: Options<ListCompaniesData, ThrowOnError>
) =>
  (options?.client ?? client).get<ListCompaniesResponse, ListCompaniesErrors, ThrowOnError>({
    url: "/api/v1/crm/company",
    ...options
  });

/**
 * Create company
 */
export const createCompany = <ThrowOnError extends boolean = false>(
  options: Options<CreateCompanyData, ThrowOnError>
) =>
  (options?.client ?? client).post<CreateCompanyResponse, CreateCompanyErrors, ThrowOnError>({
    url: "/api/v1/crm/company",
    ...options
  });

/**
 * Get company by ID
 */
export const getCompany = <ThrowOnError extends boolean = false>(options: Options<GetCompanyData, ThrowOnError>) =>
  (options?.client ?? client).get<GetCompanyResponse, GetCompanyErrors, ThrowOnError>({
    url: "/api/v1/crm/company/{id}",
    ...options
  });

/**
 * Update company
 */
export const updateCompany = <ThrowOnError extends boolean = false>(
  options: Options<UpdateCompanyData, ThrowOnError>
) =>
  (options?.client ?? client).patch<UpdateCompanyResponse, UpdateCompanyErrors, ThrowOnError>({
    url: "/api/v1/crm/company/{id}",
    ...options
  });

/**
 * Delete company
 */
export const deleteCompany = <ThrowOnError extends boolean = false>(
  options: Options<DeleteCompanyData, ThrowOnError>
) =>
  (options?.client ?? client).delete<DeleteCompanyResponse, DeleteCompanyErrors, ThrowOnError>({
    url: "/api/v1/crm/company/{id}",
    ...options
  });

Generated types:

// platform/packages/oneapp-sdk/generated/sdk/types.gen.ts

export type Company = {
  id: string;
  name: string;
  industry?: string | null;
  website?: string | null;
  revenue?: number | null;
  employees?: number | null;
  createdAt: string;
  updatedAt: string;
};

export type ListCompaniesData = {
  query?: {
    take?: number;
    skip?: number;
    industry?: string;
  };
};

export type ListCompaniesResponse = {
  data: Company[];
  total: number;
  page: number;
  pageSize: number;
  hasMore: boolean;
};

export type CreateCompanyData = {
  body: {
    name: string;
    industry?: string;
    website?: string;
    revenue?: number;
    employees?: number;
  };
};

export type CreateCompanyResponse = Company;

export type GetCompanyData = {
  path: {
    id: string;
  };
};

export type GetCompanyResponse = Company;

Use in your app:

import { listCompanies, createCompany, getCompany } from "@repo/oneapp-sdk";

// List companies
const companies = await listCompanies({
  query: { take: 10, industry: "Technology" }
});

// TypeScript knows the shape!
companies.data.data.forEach((company) => {
  console.log(company.name); // ✅ Autocomplete works
  console.log(company.foo); // ❌ Error: Property 'foo' does not exist
});

// Create company
const newCompany = await createCompany({
  body: {
    name: "Acme Inc",
    industry: "Technology",
    website: "https://acme.com",
    revenue: 1000000,
    employees: 50
  }
});

// Get company by ID
const company = await getCompany({
  path: { id: "company_123" }
});

That's it! Fully type-safe API client.

When to use what

Use SDK (client-side HTTP)

✅ Client components:

"use client";

import { useState, useEffect } from "react";
import { listCompanies } from "@repo/oneapp-sdk";

export function CompanyList() {
  const [companies, setCompanies] = useState([]);

  useEffect(() => {
    listCompanies({ query: { take: 10 } }).then((result) => {
      setCompanies(result.data.data);
    });
  }, []);

  return (
    <div>
      {companies.map((company) => (
        <div key={company.id}>{company.name}
      ))}

  );
}

✅ External applications:

// Separate service/app calling your API
import { listCompanies } from "@repo/oneapp-sdk";

const companies = await listCompanies();

✅ Browser-based apps:

// SPA, browser extension, mobile app
import { listCompanies } from "@repo/oneapp-sdk";

const companies = await listCompanies();

Use ORM (direct database access)

✅ Server components:

// app/companies/page.tsx
import { findManyCompaniesOrm } from "@repo/oneapp-shared/orm/crm";

export default async function CompaniesPage() {
  // Direct database access - faster!
  const result = await findManyCompaniesOrm();

  if (!result.success) {
    return <div>Error: {result.error.message};
  }

  return (
    <div>
      {result.data.data.map((company) => (
        <div key={company.id}>{company.name}
      ))}

  );
}

✅ Server actions:

"use server";

import { createCompanyOrm } from "@repo/oneapp-shared/orm/crm";
import { revalidatePath } from "next/cache";

export async function createCompanyAction(formData: FormData) {
  const result = await createCompanyOrm({
    name: formData.get("name") as string,
    industry: formData.get("industry") as string
  });

  if (!result.success) {
    return { error: result.error.message };
  }

  revalidatePath("/companies");
  return { success: true, data: result.data };
}

✅ API routes:

// app/api/custom/route.ts
import { findManyCompaniesOrm } from "@repo/oneapp-shared/orm/crm";

export async function GET() {
  const result = await findManyCompaniesOrm();

  if (!result.success) {
    return Response.json({ error: result.error.message }, { status: 500 });
  }

  return Response.json(result.data);
}

Performance comparison

HTTP overhead (SDK)

Client → SDK → HTTP → API → ORM → Database

  • Network latency: ~50-100ms
  • HTTP overhead: ~10-20ms
  • Total: ~60-120ms per request

Direct access (ORM)

Server → ORM → Database

  • Database query: ~5-10ms
  • Total: ~5-10ms per request

Recommendation:

  • Server-side: Use ORM directly (10-20x faster)
  • Client-side: Use SDK (no choice, need HTTP)

Best practices

1. Use SDK in client components only

// ✅ Good - client component using SDK
"use client";

import { listCompanies } from "@repo/oneapp-sdk";

export function CompanyList() {
  // SDK makes HTTP request
  const companies = await listCompanies();
}
// ❌ Bad - server component using SDK (unnecessary HTTP call)
import { listCompanies } from "@repo/oneapp-sdk";

export default async function CompaniesPage() {
  // Makes HTTP call to your own server!
  const companies = await listCompanies(); // Don't do this
}

2. Use ORM in server components

// ✅ Good - server component using ORM directly
import { findManyCompaniesOrm } from "@repo/oneapp-shared/orm/crm";

export default async function CompaniesPage() {
  // Direct database access - fast!
  const result = await findManyCompaniesOrm();
}

3. Handle AsyncResult properly

// ✅ Good - check success before using data
const result = await findManyCompaniesOrm();

if (!result.success) {
  console.error(result.error.message);
  return <div>Error loading companies;
}

// TypeScript knows result.data exists here
return <div>{result.data.data.length} companies;
// ❌ Bad - accessing data without checking success
const result = await findManyCompaniesOrm();
return <div>{result.data.data.length}; // Error if request failed!

4. Regenerate SDK after API changes

# After adding/changing API routes:

# 1. Regenerate OpenAPI paths
pnpm --filter=oneapp-api generate:openapi

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

# 3. TypeScript will error in client code if breaking changes

5. Version your API

api/v1/crm/company  # Current version
api/v2/crm/company  # New version (breaking changes)

Keep v1 running while migrating clients to v2.

Troubleshooting

SDK function not found after adding route

Problem:

import { listNewModel } from "@repo/oneapp-sdk";
// Error: Module '"@repo/oneapp-sdk"' has no exported member 'listNewModel'

Solution:

# 1. Verify route exists
ls platform/apps/oneapp-api/src/app/api/v1/path/to/route.ts

# 2. Regenerate OpenAPI paths
pnpm --filter=oneapp-api generate:openapi

# 3. Verify paths generated
cat platform/apps/oneapp-api/generated/openapi-paths.json | grep "listNewModel"

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

# 5. Restart TypeScript server in your editor
# VSCode: Cmd/Ctrl + Shift + P → "TypeScript: Restart TS Server"

Type mismatch between API and SDK

Problem:

// API returns { id: string, name: string }
// But SDK expects { id: number, name: string }

Solution:

# 1. Check Prisma schema
cat platform/packages/oneapp-shared/prisma/schemas/*.prisma

# 2. Regenerate Prisma client
pnpm --filter=@repo/oneapp-shared prisma:generate

# 3. Regenerate OpenAPI schemas
pnpm --filter=oneapp-api generate:openapi

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

500 error from API

Problem:

curl http://localhost:3001/api/v1/crm/company
# {"error":"Internal server error"}

Solution:

# 1. Check API logs
pnpm --filter=oneapp-api dev
# Look for error messages

# 2. Test ORM function directly
pnpm --filter=@repo/oneapp-shared test:orm

# 3. Check database connection
echo $DATABASE_URL
pnpm --filter=@repo/oneapp-shared prisma db push

Next steps

For Developers: Advanced API SDK patterns

Advanced integration patterns

Custom SDK client configuration

Configure base URL and headers:

// app/lib/api-client.ts
import { client } from "@repo/oneapp-sdk";

// Configure SDK client
client.setConfig({
  baseURL: process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001",
  headers: {
    "Content-Type": "application/json"
  }
});

// Add auth token interceptor
client.interceptors.request.use((config) => {
  const token = getAuthToken();
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Add error handling interceptor
client.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      // Redirect to login
      window.location.href = "/login";
    }
    return Promise.reject(error);
  }
);

React hooks for SDK

Create reusable hooks:

// app/hooks/useCompanies.ts
import { useState, useEffect } from "react";
import { listCompanies } from "@repo/oneapp-sdk";
import type { Company } from "@repo/oneapp-sdk/types";

export function useCompanies() {
  const [companies, setCompanies] = useState<Company[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    listCompanies({ query: { take: 20 } })
      .then((result) => {
        setCompanies(result.data.data);
        setLoading(false);
      })
      .catch((err) => {
        setError(err);
        setLoading(false);
      });
  }, []);

  return { companies, loading, error };
}

// Usage
function CompanyList() {
  const { companies, loading, error } = useCompanies();

  if (loading) return <div>Loading...;
  if (error) return <div>Error: {error.message};

  return (
    <div>
      {companies.map((company) => (
        <div key={company.id}>{company.name}
      ))}

  );
}

React Query integration

Use TanStack Query with SDK:

// app/hooks/useCompanies.ts
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { listCompanies, createCompany } from "@repo/oneapp-sdk";

export function useCompanies() {
  return useQuery({
    queryKey: ["companies"],
    queryFn: () => listCompanies({ query: { take: 20 } }),
  });
}

export function useCreateCompany() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (data: { name: string; industry?: string }) =>
      createCompany({ body: data }),
    onSuccess: () => {
      // Invalidate companies query to refetch
      queryClient.invalidateQueries({ queryKey: ["companies"] });
    },
  });
}

// Usage
function CompanyList() {
  const { data, isLoading, error } = useCompanies();
  const createMutation = useCreateCompany();

  const handleCreate = () => {
    createMutation.mutate({ name: "New Company", industry: "Tech" });
  };

  if (isLoading) return <div>Loading...;
  if (error) return <div>Error: {error.message};

  return (
    <div>
      <button onClick={handleCreate}>Create Company</button>
      {data?.data.data.map((company) => (
        <div key={company.id}>{company.name}
      ))}

  );
}

Optimistic updates

Update UI before server responds:

import { useMutation, useQueryClient } from "@tanstack/react-query";
import { updateCompany } from "@repo/oneapp-sdk";
import type { Company } from "@repo/oneapp-sdk/types";

export function useUpdateCompany() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: ({ id, data }: { id: string; data: Partial<Company> }) => updateCompany({ path: { id }, body: data }),

    // Optimistic update
    onMutate: async ({ id, data }) => {
      // Cancel outgoing refetches
      await queryClient.cancelQueries({ queryKey: ["companies"] });

      // Snapshot previous value
      const previous = queryClient.getQueryData(["companies"]);

      // Optimistically update
      queryClient.setQueryData(["companies"], (old: any) => ({
        ...old,
        data: {
          ...old.data,
          data: old.data.data.map((company: Company) => (company.id === id ? { ...company, ...data } : company))
        }
      }));

      return { previous };
    },

    // Rollback on error
    onError: (err, variables, context) => {
      queryClient.setQueryData(["companies"], context?.previous);
    },

    // Refetch on success or error
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ["companies"] });
    }
  });
}

Pagination with SDK

Implement cursor or offset pagination:

import { useState } from "react";
import { listCompanies } from "@repo/oneapp-sdk";
import type { Company } from "@repo/oneapp-sdk/types";

export function useCompaniesPaginated(pageSize = 20) {
  const [page, setPage] = useState(0);
  const [companies, setCompanies] = useState<Company[]>([]);
  const [hasMore, setHasMore] = useState(true);
  const [loading, setLoading] = useState(false);

  const loadMore = async () => {
    if (loading || !hasMore) return;

    setLoading(true);
    try {
      const result = await listCompanies({
        query: { take: pageSize, skip: page * pageSize },
      });

      setCompanies((prev) => [...prev, ...result.data.data]);
      setHasMore(result.data.hasMore);
      setPage((p) => p + 1);
    } finally {
      setLoading(false);
    }
  };

  const reset = () => {
    setPage(0);
    setCompanies([]);
    setHasMore(true);
  };

  return { companies, loading, hasMore, loadMore, reset };
}

// Usage
function InfiniteCompanyList() {
  const { companies, loading, hasMore, loadMore } = useCompaniesPaginated();

  return (
    <div>
      {companies.map((company) => (
        <div key={company.id}>{company.name}
      ))}

      {hasMore && (
        <button onClick={loadMore} disabled={loading}>
          {loading ? "Loading..." : "Load More"}
        </button>
      )}

  );
}

Caching strategies

Implement client-side caching:

// app/lib/api-cache.ts
class APICache {
  private cache = new Map<string, { data: any; timestamp: number }>();
  private ttl = 60000; // 1 minute

  get<T>(key: string): T | null {
    const cached = this.cache.get(key);
    if (!cached) return null;

    const age = Date.now() - cached.timestamp;
    if (age > this.ttl) {
      this.cache.delete(key);
      return null;
    }

    return cached.data as T;
  }

  set(key: string, data: any): void {
    this.cache.set(key, { data, timestamp: Date.now() });
  }

  invalidate(key: string): void {
    this.cache.delete(key);
  }

  clear(): void {
    this.cache.clear();
  }
}

export const apiCache = new APICache();

// Usage with SDK
import { listCompanies } from "@repo/oneapp-sdk";

export async function getCachedCompanies() {
  const cacheKey = "companies";

  // Check cache
  const cached = apiCache.get(cacheKey);
  if (cached) {
    return cached;
  }

  // Fetch from API
  const result = await listCompanies({ query: { take: 20 } });

  // Cache result
  apiCache.set(cacheKey, result);

  return result;
}

Error handling patterns

Standardized error handling:

// app/lib/api-errors.ts
import type { ApiError } from "@repo/oneapp-sdk/types";

export class APIError extends Error {
  constructor(
    message: string,
    public status: number,
    public code?: string
  ) {
    super(message);
    this.name = "APIError";
  }
}

export function handleAPIError(error: unknown): APIError {
  if (error instanceof APIError) {
    return error;
  }

  if (typeof error === "object" && error !== null && "response" in error) {
    const response = (error as any).response;
    return new APIError(response.data?.error || "Unknown error", response.status, response.data?.code);
  }

  return new APIError("Unknown error", 500);
}

// Usage
import { listCompanies } from "@repo/oneapp-sdk";
import { handleAPIError } from "./api-errors";

try {
  const companies = await listCompanies();
} catch (error) {
  const apiError = handleAPIError(error);

  if (apiError.status === 401) {
    // Redirect to login
  } else if (apiError.status === 404) {
    // Show not found message
  } else {
    // Generic error handling
    console.error(apiError.message);
  }
}

Batching requests

Batch multiple API calls:

// app/lib/batch-loader.ts
import { listCompanies, listContacts, listDeals } from "@repo/oneapp-sdk";

export async function loadCRMData() {
  // Execute all requests in parallel
  const [companiesResult, contactsResult, dealsResult] = await Promise.all([
    listCompanies({ query: { take: 10 } }),
    listContacts({ query: { take: 10 } }),
    listDeals({ query: { take: 10 } }),
  ]);

  return {
    companies: companiesResult.data.data,
    contacts: contactsResult.data.data,
    deals: dealsResult.data.data,
  };
}

// Usage
function CRMDashboard() {
  const [data, setData] = useState(null);

  useEffect(() => {
    loadCRMData().then(setData);
  }, []);

  if (!data) return <div>Loading...;

  return (
    <div>
      <h2>Companies: {data.companies.length}</h2>
      <h2>Contacts: {data.contacts.length}</h2>
      <h2>Deals: {data.deals.length}</h2>

  );
}

WebSocket integration

Combine REST API with WebSockets for real-time updates:

// app/lib/realtime.ts
import { listCompanies } from "@repo/oneapp-sdk";

class RealtimeService {
  private ws: WebSocket | null = null;
  private listeners = new Map<string, Set<(data: any) => void>>();

  connect() {
    this.ws = new WebSocket("ws://localhost:3001/ws");

    this.ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      const listeners = this.listeners.get(message.type);

      if (listeners) {
        listeners.forEach((listener) => listener(message.data));
      }
    };
  }

  subscribe(type: string, callback: (data: any) => void) {
    if (!this.listeners.has(type)) {
      this.listeners.set(type, new Set());
    }
    this.listeners.get(type)!.add(callback);

    return () => {
      this.listeners.get(type)?.delete(callback);
    };
  }

  disconnect() {
    this.ws?.close();
  }
}

export const realtime = new RealtimeService();

// Usage
function CompanyListRealtime() {
  const [companies, setCompanies] = useState([]);

  useEffect(() => {
    // Initial fetch
    listCompanies().then((result) => setCompanies(result.data.data));

    // Subscribe to real-time updates
    realtime.connect();
    const unsubscribe = realtime.subscribe("company.created", (newCompany) => {
      setCompanies((prev) => [...prev, newCompany]);
    });

    return () => {
      unsubscribe();
      realtime.disconnect();
    };
  }, []);

  return <div>{
    /* render companies */
  };
}

On this page