Code Structure Best Practices
This guide outlines the core principles and best practices for writing clean, maintainable code for the Employ platform. Following these guidelines will help ensure consistency across the codebase and make collaboration easier for all team members.
Core Principles
1. Component-Based Architecture
All UI should be broken down into reusable components. Pages should only use components and must not have direct implementation code.
<!-- ✅ GOOD: Page with only components -->
<template>
<ApplicationList
:applications="applications?.items"
:loading="status === 'pending'"
:error="error"
@viewApplication="viewApplication"
showPagination
/>
</template>
<!-- ❌ BAD: Page with direct implementation -->
<template>
<div>
<h1>Applications</h1>
<div v-for="application in applications" :key="application.id">
<!-- Direct implementation code instead of using components -->
<div class="card">{{ application.title }}</div>
</div>
</div>
</template>
2. Data Fetching via Composables
All data fetching logic should be extracted into composables to promote reusability and separation of concerns.
// ✅ GOOD: Using composables for data fetching
const { applicationsData, applicationsStatus, applicationsError, refreshApplications } =
useApplications(computed(() => jobId.value));
// ❌ BAD: Inline data fetching in component
const applications = ref([]);
onMounted(async () => {
try {
const response = await fetch('/api/applications');
applications.value = await response.json();
} catch (error) {
console.error(error);
}
});
3. Separation of Concerns
Each file should have a single responsibility:
- Components handle presentation
- Composables handle logic and data
- API calls are abstracted through the API caller
Real-World Example: Applications Feature
Let's look at how the Applications feature is structured following these best practices:
Page Structure (apps/web/pages/app/applications/index.vue)
<script setup lang="ts">
definePageMeta({
layout: "saas-app",
});
// Using the apiCaller pattern for data fetching
const { apiCaller } = useApiCaller();
const { currentOrganization } = await useUser();
// Fetch applications data
const { data: applications, status, error, refresh } = await useAsyncData(
`applications-${currentOrganization.value?.id}`,
async () => {
if (!currentOrganization.value?.id) return { items: [], total: 0 };
// Data fetching logic...
}
);
// Handle viewing application details
function viewApplication(applicationId: string) {
// Navigation logic...
}
// Set breadcrumbs
const { setBreadcrumbs } = useBreadcrumb();
setBreadcrumbs([
{ text: "Dashboard", href: "/app" },
{ text: "Applications" },
]);
</script>
<template>
<!-- Using a component for the UI -->
<ApplicationList
:applications="applications?.items"
:loading="status === 'pending'"
:error="error"
@viewApplication="viewApplication"
showPagination
/>
</template>
Component Structure (apps/web/modules/platform/application/components/ApplicationList.vue)
The component handles presentation, filtering, and events:
<script setup lang="ts">
// Component props define the interface
interface Props {
applications: any[];
loading?: boolean;
error?: Error | null;
// Other props...
}
const props = withDefaults(defineProps<Props>(), {
// Default values...
});
const emit = defineEmits(["viewApplication"]);
// UI helper functions
function getStatusColor(status: string) {
// Display logic...
}
// Event handlers
function viewApplication(applicationId: string) {
emit("viewApplication", applicationId);
}
</script>
<template>
<ListingContainer>
<!-- Component template structure -->
</ListingContainer>
</template>
Data Fetching Composable (apps/web/modules/platform/shared/composables/useApplications.ts)
Encapsulated data fetching logic:
export function useApplications(jobId: ComputedRef<string>) {
const { apiCaller } = useApiCaller();
const { toast } = useToast();
const { data, status, error, refresh } = useAsyncData(
`applications-${jobId.value}`,
async () => {
// Data fetching logic isolated in composable
}
);
// Error handling
watchEffect(() => {
if (error.value) {
toast({
variant: "error",
title: "Error",
description: error.value.message || "Failed to load applications",
duration: 5000,
});
}
});
return {
applicationsData: data,
applicationsStatus: status,
applicationsError: error,
refreshApplications: refresh
};
}
Best Practices Summary
- Components Only in Pages: Pages should only use components and not contain direct implementation code.
- Everything is a Component: Break UI elements into reusable, well-defined components.
- Data Fetching via Composables: Extract all data fetching logic into composables.
- Type Everything: Use TypeScript interfaces for props, emits, and return values.
- Follow the Directory Structure:
pages/: Route components that only use other componentsmodules/: Feature-specific components and logicmodules/*/components/: UI componentsmodules/*/composables/: Reusable logic
By following these guidelines, you'll contribute to a maintainable, scalable codebase that's easy for all team members to work with.