Developing Custom Modules in Directus
Module Extension is a full-featured Vue 3 page in the Directus admin panel with its own route and icon in navigation. Used for custom dashboards, specific business tools, integrations that require UI.
Module Structure
npx create-directus-extension@latest
# Type: module
# Name: sales-dashboard
directus-extension-module-sales-dashboard/
├── src/
│ ├── index.ts # Module registration
│ └── module.vue # Main component
├── package.json
└── tsconfig.json
Module Registration
// src/index.ts
import ModuleComponent from './module.vue'
export default {
id: 'sales-dashboard', // unique ID → route /sales-dashboard
name: 'Sales Dashboard',
icon: 'bar_chart',
routes: [
{
path: '',
component: ModuleComponent,
},
{
path: ':orderId',
component: () => import('./views/OrderDetail.vue'),
props: true,
},
],
}
Dashboard Component
<!-- src/module.vue -->
<template>
<private-view title="Sales Dashboard">
<template #headline>Sales Analytics</template>
<template #title-outer:prepend>
<v-button rounded icon secondary @click="refresh">
<v-icon name="refresh" />
</v-button>
</template>
<template #actions>
<v-button @click="exportData">
<v-icon name="download" left />
Export
</v-button>
</template>
<!-- Main content -->
<div class="dashboard-grid">
<div class="stat-card" v-for="stat in stats" :key="stat.label">
<div class="stat-value">{{ stat.value }}</div>
<div class="stat-label">{{ stat.label }}</div>
<div class="stat-change" :class="stat.change > 0 ? 'positive' : 'negative'">
{{ stat.change > 0 ? '+' : '' }}{{ stat.change }}%
</div>
</div>
</div>
<div class="orders-table">
<v-table
:headers="headers"
:items="orders"
:loading="loading"
show-resize
@click:row="openOrder"
/>
</div>
</private-view>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useApi, useRouter } from '@directus/extensions-sdk'
const api = useApi()
const router = useRouter()
const loading = ref(true)
const stats = ref<any[]>([])
const orders = ref<any[]>([])
const headers = [
{ text: 'ID', value: 'id', width: 80 },
{ text: 'Customer', value: 'customer_email' },
{ text: 'Total', value: 'total', width: 120 },
{ text: 'Status', value: 'status', width: 120 },
{ text: 'Date', value: 'date_created', width: 160 },
]
async function fetchData() {
loading.value = true
try {
const [ordersRes, statsRes] = await Promise.all([
api.get('/items/orders', {
params: {
sort: '-date_created',
limit: 50,
fields: ['id', 'customer_email', 'total', 'status', 'date_created'],
},
}),
api.get('/custom/reports/sales'),
])
orders.value = ordersRes.data.data
stats.value = [
{ label: 'Orders today', value: statsRes.data.today, change: statsRes.data.todayChange },
{ label: 'Monthly revenue', value: `${statsRes.data.monthRevenue.toLocaleString()} $`, change: statsRes.data.revenueChange },
]
} finally {
loading.value = false
}
}
function openOrder({ item }: { item: any }) {
router.push(`/sales-dashboard/${item.id}`)
}
async function exportData() {
const response = await api.post('/custom/reports/export', {
collection: 'orders',
format: 'csv',
}, { responseType: 'blob' })
const url = URL.createObjectURL(new Blob([response.data]))
const a = document.createElement('a')
a.href = url
a.download = 'orders.csv'
a.click()
}
function refresh() { fetchData() }
onMounted(fetchData)
</script>
<style scoped>
.dashboard-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; padding: 24px; }
.stat-card { background: var(--background-page); border: 1px solid var(--border-normal); border-radius: 8px; padding: 16px; }
.stat-value { font-size: 28px; font-weight: 700; }
.stat-label { color: var(--foreground-subdued); font-size: 13px; }
.positive { color: var(--success); }
.negative { color: var(--danger); }
.orders-table { padding: 0 24px 24px; }
</style>
Using Directus API from Module
// Access to standard Directus services
const api = useApi() // HTTP client (Axios)
const stores = useStores() // Pinia stores of Directus
const router = useRouter() // Vue Router
// Create record
await api.post('/items/articles', { data: { title: 'New', status: 'draft' } })
// Standard Directus UI components
// <v-button>, <v-input>, <v-select>, <v-table>, <v-icon>, <v-dialog>
// <private-view> — page wrapper with navigation sidebar
Timeline
Development of a custom module (dashboard with tables and export) — 2–4 days.







