DXLander implements a centralized type architecture where all domain types are defined once in packages/shared/src/types/ and shared across the entire monorepo (frontend, backend, and packages).
All domain types live in packages/shared/src/types/:
packages/shared/src/types/
├── index.ts # Core domain models (Project, User, Deployment)
├── serialized.ts # Serialized versions for API responses
├── integration-vault.ts # Integration credential vault
├── deployment.ts # Deployment credentials & platforms
├── config.ts # Build configuration types
└── ai-providers.ts # AI provider testing types
Problem: tRPC serializes Date objects to ISO strings when sending over the wire.
Solution: We provide two versions of types with Date fields:
- Backend types - Use
Dateobjects (matches database) - Frontend types - Use
stringdates (matches API response)
// Backend (packages/shared/src/types/index.ts)
export interface Project {
id: string;
name: string;
createdAt: Date; // ← Date object
updatedAt: Date; // ← Date object
}
// Frontend (packages/shared/src/types/serialized.ts)
export type SerializedProject = Omit<Project, 'createdAt' | 'updatedAt'> & {
createdAt: string; // ← ISO string
updatedAt: string; // ← ISO string
};ESLint automatically catches duplicate type definitions:
// eslint.config.mjs
'no-restricted-syntax': [
'error',
{
selector: 'TSInterfaceDeclaration[id.name=/^(Project|Deployment|User|...)$/]',
message: '❌ Do not redefine domain types. Import from @dxlander/shared instead.',
},
// ... same for type aliases
]Result: Developers get immediate feedback when trying to redefine types.
Main domain types with Date objects (for backend use):
Project- Project entityUser- User entityDeployment- Deployment entityProjectFile- File in a project- Plus: Zod schemas, input types, etc.
Serialized versions with string dates (for frontend use):
SerializedProjectSerializedUserSerializedDeploymentSerializedConfigSetSerializedIntegrationVaultEntrySerializedDeploymentCredential
When to use: Backend services working with database records.
import type { Project, User, Deployment } from '@dxlander/shared';
export class ProjectService {
async getProject(id: string): Promise<Project> {
const project = await db.query.projects.findFirst({ where: eq(projects.id, id) });
return project; // Date objects from database
}
}Key Domain Types:
Project- Project entityUser- User entityDeployment- Deployment entityIntegrationVaultEntry- Integration credentialsDeploymentCredential- Deployment platform credentialsConfigSet- Configuration set
When to use: Frontend components receiving data from tRPC.
import type { SerializedProject } from '@dxlander/shared';
import { formatDistanceToNow } from 'date-fns';
export default function ProjectCard({ project }: { project: SerializedProject }) {
return (
<div>
<h3>{project.name}</h3>
<p>Created {formatDistanceToNow(new Date(project.createdAt), { addSuffix: true })}</p>
</div>
);
}When to use: Creating or updating resources with Zod validation.
import { CreateProjectSchema, type CreateProjectInput } from '@dxlander/shared';
export const projectsRouter = router({
create: protectedProcedure
.input(CreateProjectSchema)
.mutation(async ({ input, ctx }): Promise<Project> => {
// input is validated and typed
const project = await projectService.create(input);
return project;
}),
});When to use: Service-specific functionality.
import type { DeploymentPlatform, ConfigType, ProviderTestConfig } from '@dxlander/shared';
// Deployment platform selection
const platform: DeploymentPlatform = 'vercel';
// Config generation
const configType: ConfigType = 'docker';
// AI provider testing
const testConfig: ProviderTestConfig = {
provider: 'claude-code',
apiKey: process.env.ANTHROPIC_API_KEY,
settings: { model: 'claude-sonnet-4' },
};// apps/api/src/routes/projects.ts
import { router, protectedProcedure, IdSchema } from '@dxlander/shared';
import type { Project } from '@dxlander/shared';
export const projectsRouter = router({
get: protectedProcedure.input(IdSchema).query(async ({ input, ctx }): Promise<Project> => {
// Returns Project with Date objects
const project = await projectService.getById(input.id);
return project;
}),
});Key Points:
- Return type is
Project(withDateobjects) - tRPC automatically serializes dates to strings
- Frontend receives
SerializedProject
// apps/web/app/dashboard/page.tsx
import type { SerializedProject } from '@dxlander/shared';
import { formatDistanceToNow } from 'date-fns';
export default function Dashboard() {
// Type is automatically inferred as SerializedProject[]
const { data: projects } = trpc.projects.list.useQuery();
return (
<div>
{projects?.map(project => (
<ProjectCard key={project.id} project={project} />
))}
</div>
);
}
function ProjectCard({ project }: { project: SerializedProject }) {
return (
<div>
<h3>{project.name}</h3>
<p>
Created {formatDistanceToNow(new Date(project.createdAt), { addSuffix: true })}
</p>
</div>
);
}Key Points:
- Use
SerializedProjecttype - Dates are strings (ISO format)
- Convert to
Datefor formatting withdate-fns
// apps/web/app/dashboard/page.tsx
import type { SerializedProject } from '@dxlander/shared';
// ✅ GOOD - Extend with UI-specific fields
interface DashboardProject extends SerializedProject {
isSelected?: boolean; // UI state
lastActivity?: string; // Computed field
}
// ❌ BAD - Redefinition (ESLint will catch)
interface Project {
id: string;
name: string;
// ... redefinition of all fields
}Key Points:
- Use
extendsor&to add fields - Don't redefine the entire type
- Only add UI-specific or computed fields
// ✅ GOOD - Type alias for convenience
import type { SerializedProject } from '@dxlander/shared';
type Project = SerializedProject;
// ❌ BAD - Redefinition
type Project = {
id: string;
name: string;
// ...
};// apps/api/src/services/project.service.ts
import type { Project, CreateProjectInput, UpdateProjectInput } from '@dxlander/shared';
import { db, schema } from '@dxlander/database';
export class ProjectService {
async create(input: CreateProjectInput): Promise<Project> {
const [project] = await db
.insert(schema.projects)
.values({
...input,
createdAt: new Date(),
updatedAt: new Date(),
})
.returning();
return project;
}
async update(id: string, input: UpdateProjectInput): Promise<Project> {
const [project] = await db
.update(schema.projects)
.set({ ...input, updatedAt: new Date() })
.where(eq(schema.projects.id, id))
.returning();
return project;
}
}-
Import from Shared
import type { Project, SerializedProject } from '@dxlander/shared';
-
Use Serialized Types in Frontend
const { data: projects } = trpc.projects.list.useQuery(); // projects: SerializedProject[]
-
Use date-fns for Formatting
import { formatDistanceToNow, format } from 'date-fns'; formatDistanceToNow(new Date(project.createdAt), { addSuffix: true });
-
Extend Types When Needed
interface UIProject extends SerializedProject { isSelected: boolean; }
-
Add JSDoc to New Types
/** * My new type description * * @example * ```typescript * const example: MyType = { ... }; * ``` */ export interface MyType { ... }
-
Use Zod Schemas for Validation
export const CreateMyTypeSchema = z.object({ ... }); export type CreateMyTypeInput = z.infer<typeof CreateMyTypeSchema>;
-
Don't Redefine Domain Types
// ❌ BAD interface Project { // ESLint error! id: string; }
-
Don't Use any for API Responses
// ❌ BAD const { data: projects } = trpc.projects.list.useQuery() as any;
-
Don't Skip Serialization
// ❌ BAD - Using Date type in frontend function Component({ project }: { project: Project }) { // project.createdAt is string, not Date! }
-
Don't Use Custom Date Utilities
// ❌ BAD - Custom utilities removed import { formatRelativeTime } from '@dxlander/shared/utils'; // ✅ GOOD - Use date-fns import { formatDistanceToNow } from 'date-fns';
❌ Problem:
// Frontend component
import type { Project } from '@dxlander/shared';
function ProjectCard({ project }: { project: Project }) {
// project.createdAt is actually a string, not Date!
return <div>{project.createdAt.toISOString()}</div>; // ❌ Error!
}✅ Solution:
import type { SerializedProject } from '@dxlander/shared';
import { formatDistanceToNow } from 'date-fns';
function ProjectCard({ project }: { project: SerializedProject }) {
return <div>{formatDistanceToNow(new Date(project.createdAt))}</div>;
}❌ Problem:
// Frontend component
type Project = {
// ❌ ESLint error!
id: string;
name: string;
// ... 15 more fields
};✅ Solution:
import type { SerializedProject } from '@dxlander/shared';
type Project = SerializedProject; // ✅ Type alias is OK❌ Problem:
export const projectsRouter = router({
create: protectedProcedure
.input(z.object({ name: z.string() })) // ❌ Inline schema
.mutation(async ({ input }) => { ... }),
});✅ Solution:
import { CreateProjectSchema } from '@dxlander/shared';
export const projectsRouter = router({
create: protectedProcedure
.input(CreateProjectSchema) // ✅ Reusable schema
.mutation(async ({ input }) => { ... }),
});❌ Problem:
interface User {
// ❌ ESLint: Do not redefine domain types
id: string;
email: string;
}
// Developer ignores error and commits✅ Solution:
import type { SerializedUser } from '@dxlander/shared';
// ✅ Use shared type instead