Skip to main content

Getting Started

Get up and running with Codella core services in your project.

Prerequisites

  • Node.js 18+
  • npm, yarn, or pnpm
  • TypeScript 5+ (recommended)
  • React 18+ (for React projects)

Installation

Install Core Package

npm install @codella-software/utils

Peer dependencies you may need:

npm install rxjs yup

The core package includes:

  • FormBuilder - Type-safe form configuration
  • TableBuilder - Dynamic table configuration
  • FiltersAndSortService - Filtering and sorting state management

All core services are framework-agnostic and built on RxJS Observables.

Using with shadcn/ui

If you have a React project with shadcn/ui already installed, here's how to integrate Codella:

Step 1: Install Codella

npm install @codella-software/utils rxjs yup

Step 2: Create a Form with Your Types

// types/user.ts
export interface CreateUserForm {
name: string;
email: string;
role: 'user' | 'admin';
active: boolean;
}

Step 3: Build the Form Configuration

// forms/userForm.ts
import { FormBuilder } from '@codella-software/utils';
import { CreateUserForm } from '@/types/user';

export const userFormConfig = new FormBuilder<CreateUserForm>()
.addField({
name: 'name',
type: 'text',
label: 'Full Name',
validation: { required: true, minLength: 2 },
})
.addField({
name: 'email',
type: 'email',
label: 'Email Address',
validation: { required: true, email: true },
})
.addField({
name: 'role',
type: 'select',
label: 'Role',
options: [
{ value: 'user', label: 'User' },
{ value: 'admin', label: 'Admin' },
],
validation: { required: true },
})
.addField({
name: 'active',
type: 'checkbox',
label: 'Active',
})
.build();

Step 4: Create a React Component Using shadcn Components

// components/UserForm.tsx
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Checkbox } from '@/components/ui/checkbox';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { userFormConfig } from '@/forms/userForm';
import { CreateUserForm } from '@/types/user';

export function UserForm() {
const [isSubmitting, setIsSubmitting] = useState(false);

// Use the form config to guide your form
const form = useForm<CreateUserForm>({
defaultValues: {
name: '',
email: '',
role: 'user',
active: true,
},
});

async function onSubmit(values: CreateUserForm) {
setIsSubmitting(true);
try {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(values),
});

if (response.ok) {
console.log('User created successfully');
form.reset();
}
} finally {
setIsSubmitting(false);
}
}

return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Full Name</FormLabel>
<FormControl>
<Input placeholder="John Doe" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="john@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<FormField
control={form.control}
name="role"
render={({ field }) => (
<FormItem>
<FormLabel>Role</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a role" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="user">User</SelectItem>
<SelectItem value="admin">Admin</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>

<FormField
control={form.control}
name="active"
render={({ field }) => (
<FormItem className="flex flex-row items-center space-x-3 space-y-0">
<FormControl>
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
<FormLabel className="font-normal cursor-pointer">
Active user
</FormLabel>
</FormItem>
)}
/>

<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Creating...' : 'Create User'}
</Button>
</form>
</Form>
);
}

Step 5: Use in Your App

// app/page.tsx
import { UserForm } from '@/components/UserForm';

export default function Home() {
return (
<div className="container mx-auto py-10">
<h1 className="text-3xl font-bold mb-8">Create New User</h1>
<div className="max-w-md">
<UserForm />
</div>
</div>
);
}

How It Works

  1. Define your data types - Use TypeScript interfaces for type safety
  2. Build configurations - Use FormBuilder, TableBuilder, etc. to define your UI structure
  3. Render with shadcn - Use shadcn/ui components to render the configured forms/tables
  4. Manage state - Use React hooks (useState, useForm, etc.) to manage the actual state

The Codella builders provide:

  • ✅ Type-safe configuration
  • ✅ Validation rules
  • ✅ Field structure
  • ✅ State management (via RxJS Observables)
  • ✅ Filtering and sorting logic

You bring:

  • Your own UI components (shadcn/ui, Material UI, etc.)
  • Form handling (react-hook-form, Formik, etc.)
  • State management (React hooks, Zustand, etc.)

Your First Form

Step 1: Define Your Form Type

interface UserForm {
name: string;
email: string;
age: number;
newsletter: boolean;
}

Step 2: Build Form Configuration

import { FormBuilder } from '@codella-software/utils';

const formConfig = new FormBuilder<UserForm>()
.addField({
name: 'name',
type: 'text',
label: 'Full Name',
validation: { required: true, minLength: 2 },
})
.addField({
name: 'email',
type: 'email',
label: 'Email Address',
validation: { required: true, email: true },
})
.addField({
name: 'age',
type: 'number',
label: 'Age',
validation: { required: true, min: 18, max: 120 },
})
.addField({
name: 'newsletter',
type: 'checkbox',
label: 'Subscribe to newsletter',
})
.build();

// Access generated configuration
console.log(formConfig.fields);
console.log(formConfig.steps); // For multi-step forms

Your First Table

Step 1: Define Your Data Type

interface User {
id: number;
name: string;
email: string;
status: 'active' | 'inactive';
}

Step 2: Build Table Configuration

import { TableBuilder } from '@codella-software/utils';

const tableConfig = new TableBuilder<User>()
.addColumn({
id: 'id',
header: 'ID',
accessorKey: 'id',
sortable: true,
})
.addColumn({
id: 'name',
header: 'Name',
accessorKey: 'name',
sortable: true,
})
.addColumn({
id: 'email',
header: 'Email',
accessorKey: 'email',
sortable: true,
})
.addColumn({
id: 'status',
header: 'Status',
accessorKey: 'status',
})
.enableSorting()
.enableFiltering()
.enablePagination({ pageSize: 10 })
.build();

// Use configuration
console.log(tableConfig.columns);
console.log(tableConfig.pagination);

Step 3: Create a shadcn Table Component

// components/UsersTable.tsx
import { useState, useMemo } from 'react';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { tableConfig } from '@/tables/userTable';
import { User } from '@/types/user';

interface UsersTableProps {
users: User[];
}

export function UsersTable({ users }: UsersTableProps) {
const [sortBy, setSortBy] = useState<keyof User>('name');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc');
const [pageSize, setPageSize] = useState(10);
const [currentPage, setCurrentPage] = useState(0);

// Sort users
const sorted = useMemo(() => {
const sorted = [...users].sort((a, b) => {
const aVal = a[sortBy];
const bVal = b[sortBy];
if (aVal < bVal) return sortOrder === 'asc' ? -1 : 1;
if (aVal > bVal) return sortOrder === 'asc' ? 1 : -1;
return 0;
});
return sorted;
}, [users, sortBy, sortOrder]);

// Paginate
const paginated = useMemo(() => {
const start = currentPage * pageSize;
return sorted.slice(start, start + pageSize);
}, [sorted, currentPage, pageSize]);

return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<div className="flex gap-2">
<Select value={sortBy} onValueChange={(v) => setSortBy(v as keyof User)}>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
{tableConfig.columns.map((col) => (
<SelectItem key={col.id} value={col.id}>
{col.header}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
variant="outline"
onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}
>
{sortOrder === 'asc' ? '↑' : '↓'}
</Button>
</div>
<Select value={pageSize.toString()} onValueChange={(v) => setPageSize(parseInt(v))}>
<SelectTrigger className="w-24">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="5">5 rows</SelectItem>
<SelectItem value="10">10 rows</SelectItem>
<SelectItem value="25">25 rows</SelectItem>
</SelectContent>
</Select>
</div>

<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
{tableConfig.columns.map((col) => (
<TableHead key={col.id}>{col.header}</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{paginated.length === 0 ? (
<TableRow>
<TableCell colSpan={tableConfig.columns.length} className="text-center">
No users found
</TableCell>
</TableRow>
) : (
paginated.map((user) => (
<TableRow key={user.id}>
<TableCell>{user.id}</TableCell>
<TableCell>{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>
<Badge variant={user.status === 'active' ? 'default' : 'secondary'}>
{user.status}
</Badge>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>

<div className="flex justify-between items-center">
<p className="text-sm text-gray-600">
Showing {currentPage * pageSize + 1} to {Math.min((currentPage + 1) * pageSize, sorted.length)} of{' '}
{sorted.length} users
</p>
<div className="flex gap-2">
<Button
variant="outline"
disabled={currentPage === 0}
onClick={() => setCurrentPage(currentPage - 1)}
>
Previous
</Button>
<Button
variant="outline"
disabled={(currentPage + 1) * pageSize >= sorted.length}
onClick={() => setCurrentPage(currentPage + 1)}
>
Next
</Button>
</div>
</div>
</div>
);
}

Step 4: Define Table Configuration

// tables/userTable.ts
import { TableBuilder } from '@codella-software/utils';
import { User } from '@/types/user';

export const tableConfig = new TableBuilder<User>()
.addColumn({
id: 'id',
header: 'ID',
accessorKey: 'id',
sortable: true,
})
.addColumn({
id: 'name',
header: 'Name',
accessorKey: 'name',
sortable: true,
})
.addColumn({
id: 'email',
header: 'Email',
accessorKey: 'email',
sortable: true,
})
.addColumn({
id: 'status',
header: 'Status',
accessorKey: 'status',
})
.enableSorting()
.enablePagination({ pageSize: 10 })
.build();

Step 5: Use in Your App

// pages/users.tsx
import { useState, useEffect } from 'react';
import { UsersTable } from '@/components/UsersTable';
import { User } from '@/types/user';

export default function UsersPage() {
const [users, setUsers] = useState<User[]>([]);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
// Fetch users from your API
fetch('/api/users')
.then((res) => res.json())
.then((data) => {
setUsers(data);
setIsLoading(false);
});
}, []);

if (isLoading) return <div>Loading...</div>;

return (
<div className="container mx-auto py-10">
<h1 className="text-3xl font-bold mb-8">Users</h1>
<UsersTable users={users} />
</div>
);
}

Standalone Configuration Example

If you just want to explore the builders without shadcn/ui:

Create a Filter Service

import { FiltersAndSortService } from '@codella-software/utils';

interface Filters {
status?: string;
name?: string;
}

const filterService = new FiltersAndSortService<Filters>({
storageKey: 'user-filters',
defaultFilters: { status: '', name: '' },
persistToStorage: true
});

// Subscribe to state changes
filterService.getState().subscribe((state) => {
console.log('Current filters:', state.filters);
console.log('Current sort:', state.sort);
console.log('Current pagination:', state.pagination);
});

// Update filters
filterService.setFilters({ status: 'active' });

// Add another filter (merged with existing)
filterService.setFilters({ name: 'john' });

// Set sort
filterService.setSort('email');

// Set pagination
filterService.setPagination(1, 20);

// Apply filters and sort to your data
const users: User[] = [/* your data */];
const filtered = users.filter(user => {
const state = filterService.getCurrentState();
if (state.filters.status && user.status !== state.filters.status) return false;
if (state.filters.name && !user.name.toLowerCase().includes(state.filters.name)) return false;
return true;
});

Using the API Service

The APIService provides a type-safe, provider-agnostic HTTP client with built-in interceptors, middleware hooks, and error handling.

Basic Setup

import { createApiClient } from '@codella-software/utils';
import { FirebaseAuthProvider } from '@codella-software/utils';

// Initialize the API client
const api = createApiClient({
baseUrl: 'https://api.example.com',
authProvider: new FirebaseAuthProvider(auth), // Your auth provider
hooks: {
onBeforeRequest: (ctx) => {
console.log('Sending request to:', ctx.config.url);
},
onAfterResponse: (ctx) => {
console.log(`Response received (${ctx.duration}ms)`);
},
onError: (ctx) => {
console.error('Request failed:', ctx.error.message);
},
}
});

// Make requests
const users = await api.get('/users');
const createdUser = await api.post('/users', { name: 'John', email: 'john@example.com' });

Request/Response Interceptors

Interceptors allow you to transform requests and responses, add headers, or handle errors globally.

// Add request interceptor to include custom headers
api.interceptors.request.use((config) => ({
...config,
headers: {
...config.headers,
'X-API-Key': process.env.REACT_APP_API_KEY,
'X-Request-ID': generateUUID(),
}
}));

// Add response interceptor to transform data
api.interceptors.response.use((response) => ({
...response,
data: response.data.result ?? response.data, // Unwrap nested data
}));

// Add error interceptor to handle specific error statuses
api.interceptors.error.use((error) => {
if (error.response?.status === 401) {
// Handle unauthorized - trigger re-authentication
window.location.href = '/login';
}
throw error;
});

Using in React

import { useAPIService } from '@codella-software/react';
import { useEffect, useState } from 'react';

function UsersList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const api = useAPIService({
baseUrl: 'https://api.example.com',
hooks: {
onBeforeRequest: () => setLoading(true),
onAfterResponse: () => setLoading(false),
onError: (ctx) => setError(ctx.error.message),
onRetry: (ctx) => console.log(`Retrying (attempt ${ctx.attempt})...`),
}
});

useEffect(() => {
api.get('/users')
.then(setUsers)
.catch((err) => setError(err.message));
}, [api]);

if (loading) return <p>Loading users...</p>;
if (error) return <p className="text-red-500">Error: {error}</p>;

return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name} ({user.email})</li>
))}
</ul>
);
}

Monitoring & Analytics

Use middleware hooks to track API performance and analytics:

import { createApiClient } from '@codella-software/utils';

const api = createApiClient({
baseUrl: 'https://api.example.com',
authProvider: myAuthProvider,
hooks: {
onBeforeRequest: (ctx) => {
// Track request starts for performance monitoring
analytics.trackEvent('api_request_start', {
endpoint: ctx.config.url,
method: ctx.config.method,
});
},
onAfterResponse: (ctx) => {
// Track response times and status
analytics.trackEvent('api_request_complete', {
endpoint: ctx.config.url,
status: ctx.response.status,
duration: ctx.duration,
});
},
onError: (ctx) => {
// Track errors for error monitoring service
errorTracker.captureException(ctx.error, {
endpoint: ctx.config.url,
attempt: ctx.attempt,
});
},
}
});

For detailed API service documentation including authentication patterns, custom providers, and advanced use cases, see API Service Guide.

Multi-Step Forms

import { FormBuilder } from '@codella-software/utils';

const multiStepForm = new FormBuilder<UserForm>()
.addField({
name: 'name',
type: 'text',
label: 'Name'
})
.addField({
name: 'email',
type: 'email',
label: 'Email',
step: 1,
})
.addField({
name: 'age',
type: 'number',
label: 'Age',
step: 2,
})
.addField({
name: 'newsletter',
type: 'checkbox',
label: 'Newsletter',
step: 2,
})
.build();

// Access steps
console.log(multiStepForm.steps); // [1, 2]

Form Validation

Built-in Validators

const form = new FormBuilder<FormData>()
.addField({
name: 'email',
type: 'email',
label: 'Email',
validation: {
required: true,
email: true, // Email format validation
},
})
.addField({
name: 'password',
type: 'password',
label: 'Password',
validation: {
required: true,
minLength: 8,
maxLength: 100,
pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, // Custom regex
},
})
.addField({
name: 'age',
type: 'number',
label: 'Age',
validation: {
required: true,
min: 18,
max: 120,
},
})
.build();

Custom Validators with Yup

import * as yup from 'yup';

const customSchema = yup.object({
username: yup
.string()
.required()
.min(3)
.matches(/^[a-zA-Z0-9_]+$/, 'Only alphanumeric and underscore allowed'),
confirmPassword: yup
.string()
.oneOf([yup.ref('password')], 'Passwords must match'),
});

// Integrate with FormBuilder
const form = new FormBuilder<FormData>()
.addField({
name: 'username',
type: 'text',
label: 'Username',
})
.setValidationSchema(customSchema)
.build();

Next Steps

Troubleshooting

TypeScript Errors

If you see TypeScript errors about missing types, make sure you're using TypeScript 5+ and have strict mode enabled in your tsconfig.json:

{
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"moduleResolution": "bundler"
}
}

RxJS Errors

The core package uses RxJS 7.x. If you get peer dependency warnings, install it:

npm install rxjs@^7.8.0

Validation Not Working

Make sure to install Yup if using validation:

npm install yup