Appearance
Shared Packages
Monorepo packages for code sharing across applications.
Current Packages
packages/
├── api-client/ # Shared HTTP client for frontend apps
├── ui/ # Shared UI components (shadcn/ui)
├── eslint-config/ # ESLint configurations
├── typescript-config/ # TypeScript configurations
├── server-types/ # Server type exports
├── shared-types/ # Shared TypeScript types
└── vitest-config/ # Vitest test configurationsPackage Details
@jubiloop/api-client
Centralized API client package that eliminates code duplication across frontend applications by providing a unified interface for API communication, authentication, and server state management.
Features
- Core Client (
createApiClient): Axios-based HTTP client with automatic CSRF protection - Package Factory (
createApiPackage): Complete API setup requiring only baseURL configuration - Better Auth Integration: Authentication client with organization plugin support
- Session Support: Cookie-based authentication with
withCredentialsenabled by default - Error Handling: Configurable 401 response handling for auth invalidation
- TypeScript Support: Full type safety with interfaces for all configurations
- TanStack Query Hooks: Pre-configured hooks for auth, organizations, health, and newsletter
- Centralized Query Keys: Static query keys in
defaultQueryKeysfor consistent cache management
Architecture Benefits
- Consistency: All frontend apps use the same underlying HTTP client
- Flexibility: Each app can customize behavior while maintaining shared core
- Maintainability: Single source of truth for API communication logic
- Extensibility: Easy to add new features that benefit all apps
Usage Examples
typescript
// apps/webapp/src/lib/api/setup.ts
import { createApiPackage } from '@jubiloop/api-client'
import { queryKeys } from '@/integrations/tanstack-query/keys' // Re-exported from @jubiloop/api-client
export const api = createApiPackage({
baseURL: import.meta.env.VITE_API_URL || 'https://api.jubiloop.localhost',
onUnauthorized: () => {
// Invalidate auth queries on 401
queryClient.invalidateQueries({ queryKey: queryKeys.auth.all })
},
})
// apps/marketing/src/lib/api/setup.ts
import { createApiPackage } from '@jubiloop/api-client'
export const api = createApiPackage({
baseURL: process.env.NEXT_PUBLIC_API_URL || 'https://api.jubiloop.localhost',
// No auth handling needed for marketing site
})
// Using the hooks in components
import { api } from '@/lib/api/setup'
function MyComponent() {
const { data: health } = api.hooks.health.useHealthCheck()
const { subscribe } = api.hooks.newsletter.useNewsletter()
const { user, isAuthenticated } = api.hooks.auth.useAuth()
}Package Structure
packages/api-client/
├── src/
│ ├── auth/ # Better Auth integration
│ │ ├── client.ts # Auth client setup
│ │ └── types.ts # Auth type definitions
│ ├── core/ # Core client implementation
│ │ └── client.ts # Axios client factory
│ ├── hooks/ # React Query hooks
│ │ ├── auth.ts # Authentication hooks
│ │ ├── health.ts # Health check hooks
│ │ ├── newsletter.ts # Newsletter subscription hooks
│ │ └── organization.ts # Organization management hooks
│ ├── queries/ # TanStack Query options
│ │ ├── health.ts # Health query options
│ │ ├── organization.ts # Organization queries (moved from webapp)
│ │ └── session.ts # Session queries (moved from webapp)
│ ├── keys/ # Query key management
│ │ └── defaults.ts # Static defaultQueryKeys object
│ ├── routes/ # API endpoint definitions
│ │ └── index.ts # Centralized route paths
│ └── index.ts # Main package exports
├── tsup.config.ts # Build configuration
└── package.json # DependenciesMigration from Duplicate Code
The api-client package consolidates previously duplicated code:
Before: Each app maintained its own:
- API client setup
- Auth queries and hooks
- Health check implementations
- Newsletter subscription logic
After: Single package provides everything:
- Webapp imports from
@jubiloop/api-client - Marketing imports from
@jubiloop/api-client - Query keys centralized in
defaultQueryKeys - Newsletter renamed from "subscription" for clarity
Build System
Uses tsup for bundling:
- Multiple entry points for optimal tree-shaking
- ESM and CommonJS outputs
- TypeScript declarations
- Separate builds for different import paths
@jubiloop/ui
Centralized UI component library using shadcn/ui. See UI Components for full documentation.
@jubiloop/eslint-config
Shared ESLint configurations:
base.js- Base JavaScript/TypeScript rulesnext.js- Next.js-specific rulesreact-internal.js- React rulestanstack.js- TanStack-specific rules
@jubiloop/typescript-config
Shared TypeScript configurations:
base.json- Base configurationnextjs.json- Next.js applicationsreact-library.json- React libraries
@jubiloop/server-types
Server-side TypeScript type definitions exported from the backend.
@jubiloop/shared-types
Centralized TypeScript type definitions shared between frontend and backend applications, eliminating duplication and ensuring type consistency across the stack.
Purpose
The shared-types package solves the problem of duplicate type definitions between backend and frontend by providing a single source of truth for all shared TypeScript interfaces and types.
Features
- Entity Interfaces: Core domain models (e.g.,
IUser,IOrganization) - API Response Types: Standardized API response structures (
IApiResponse,IApiMessage,IApiError) - DTOs: Data Transfer Objects for API communication (
IUserMinimalDTO) - Request Types: API request interfaces (
ISubscribeNewsletterRequest) - Health Check Types: System health monitoring interfaces (
IHealthResponse,IDetailedHealthResponse)
Architecture
The package uses wildcard exports instead of barrel exports to:
- Avoid circular dependency issues
- Enable better tree-shaking
- Provide cleaner import paths
Build System
The package employs a two-step build process:
Production Build (npm run build):
- Uses tsup only
- Generates JavaScript and type definitions
- No declaration maps for security (prevents source exposure)
Development Build (npm run dev):
- Uses tsup + tsc concurrently
- Generates declaration maps (
.d.ts.mapfiles) - Enables IDE "Go to Definition" navigation to source TypeScript files
Package Structure
packages/shared-types/
├── src/
│ ├── entities/ # Domain entity interfaces
│ │ └── user.ts # IUser interface
│ ├── dtos/ # Data Transfer Objects
│ │ └── user.ts # User DTOs
│ ├── api/
│ │ ├── requests/ # API request types
│ │ │ └── newsletter.ts
│ │ └── responses/ # API response types
│ │ ├── common.ts # IApiResponse, IApiError
│ │ └── health.ts # Health check types
│ └── index.ts # Re-exports (minimal)
├── tsup.config.ts # Build configuration
└── package.json # Package configurationUsage Examples
typescript
// Import from specific paths (recommended)
import type { IUser } from '@jubiloop/shared-types/entities/user'
import type { IApiResponse } from '@jubiloop/shared-types/api/responses/common'
import type { IUserMinimalDTO } from '@jubiloop/shared-types/dtos/user'
// Backend: Type-safe API responses
const response: IApiResponse<IUser> = {
success: true,
data: user,
message: { type: 'success', text: 'User fetched' },
}
// Frontend: Type-safe API consumption
const { data }: IApiResponse<IUser> = await api.get('/users/me')TypeScript Naming Conventions
The package follows these conventions for clarity:
Iprefix for interfaces:IUser,IApiClient,IHealthResponseTprefix for type aliases:TSession,TAuthResponse(if needed)
This helps distinguish between interfaces and types at a glance.
@jubiloop/vitest-config
Test configuration for Vitest (currently unused as tests are not yet implemented).
Using Packages
Packages are linked using npm workspaces:
json
{
"dependencies": {
"@jubiloop/ui": "*",
"@jubiloop/eslint-config": "*"
}
}Best Practices
- Keep packages focused on truly shared code
- Avoid app-specific logic in packages
- Use TypeScript for all package code
- Document public APIs with JSDoc comments