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 expectsUserResponse, 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:generate2. 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 build3. 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:#333Architecture layers
5 layers of type safety:
- Database — PostgreSQL with Prisma schema (source of truth)
- ORM — Generated functions with
AsyncResult<T>return types - API — Next.js 16 routes using ORM functions
- OpenAPI — Auto-generated spec describing all endpoints
- 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/users → findManyUsersOrm()
// 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 existLayer 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:generateThis creates:
- Prisma Client —
generated/client/(ORM) - OpenAPI Schemas —
generated/openapi/(286 types) - 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); // numberThat'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.tsExample 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:3001Test endpoint:
curl http://localhost:3001/api/v1/crm/company?take=5That'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:openapiThis scans all route.ts files and:
- Detects HTTP methods (GET, POST, PATCH, DELETE)
- Extracts route patterns (
/api/v1/crm/company) - Identifies ORM functions used (
findManyCompaniesOrm→ Company model) - 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 specThat'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 buildThis:
- Merges schemas + paths into complete OpenAPI spec
- Runs
@hey-api/openapi-tsto generate TypeScript client - 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 changes5. 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 build500 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 pushNext steps
- Learn error handling: Error Handling →
- Explore API structure: oneapp-api Documentation →
- Understand ORM: oneapp-shared Package →
- Read conventions: Coding Conventions →
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 */
};
}Related documentation
- Error Handling: AsyncResult Pattern →
- API Server: oneapp-api Documentation →
- ORM Package: oneapp-shared Package →
- Type Safety: Type Safety Guide →
- Testing APIs: Testing & QA →
Team Workspaces
Set up isolated team development environments in the OneApp monorepo — from directory structure to dependency management, with complete configuration examples.
Cookbook & Recipes
Real-world code recipes combining multiple OneApp packages — complete examples for authentication, analytics, AI, feature flags, and more.