Add a module
A module is a folder. The CLI writes one from a real row of your API, and you finish the parts only you know.
With the CLI
The generator reads a JSON file you write, never your running API, and it infers nothing from a field's name.
Describe one row
Run the command once. If smart-admin.generate.json does not exist, the CLI copies it from smart-admin.generate.example.json and adds it to .gitignore, so a real API response never lands in your repository.
npm run generate -- postOpen the file it created. Each top-level key is a module name, and the name is the marker: generate post reads the post block.
{
"post": {
"rowsPath": "data.items",
"idKey": "id",
"row": {
"id": 41,
"title": "How we cut our build time in half",
"cover": "https://cdn.example.com/covers/41.webp",
"status": "published",
"excerpt": "A short summary shown in listings.",
"views": 1284,
"author": { "id": 7, "name": "Amina Haddad" },
"tags": ["engineering", "performance"],
"published_at": "2026-03-01T09:30:00Z"
},
"annotations": {
"cover": { "type": "image" },
"published_at": { "type": "date" },
"status": { "type": "status", "options": ["draft", "published", "archived"] },
"excerpt": { "nullable": true },
"views": { "sometimes": true },
"author": { "nested": "name" }
},
"filterFields": [
{ "key": "status", "type": "select" },
{ "key": "title", "type": "text" },
{ "key": "published_at", "type": "date" }
]
}
}Paste a row that has every field. A key that is missing here does not exist as far as the generator is concerned.
rowsPath is where the array sits in the raw response, dot-separated: data.items for { data: { items: [] } }, or "" when the response is the array itself. idKey names the identifying field when it is not id.
annotations carry what a shape cannot say. A date, an image URL and a status are all just strings on the wire, so you declare them: type (one of text, number, date, datetime, image, status, boolean), options for a status or a select filter, nullable, sometimes for a field only some responses carry, and nested for the sub-key to show from an object.
Check the plan
npm run generate -- post --dry-runIt prints every file it would write and every file it would edit, and writes nothing. It refuses before touching anything if the module already exists, if the route is taken, or if the translation namespace is in use.
Generate
npm run generate -- post --storeGenerating module "post"
from the "post" block in smart-admin.generate.json
route /posts · translations under post_module
created src/modules/post/index.ts
created src/modules/post/pages/index.vue
created src/modules/post/types/post.schemas.ts
created src/modules/post/types/post.types.ts
edited src/locales/*.json — 9 locale file(s)
edited src/main.ts
edited src/layouts/navItems.ts--store adds a Pinia store skeleton and --no-store skips it. With neither flag the command asks, unless there is no interactive terminal, in which case it skips the store and says so. --plural overrides the pluralized form, which is also the route: --plural categories gives /categories.
Restart the dev server
npm run devunplugin-vue-router reads the route table at boot, so a new page appears only after a restart.
Fill in the fetch
Open src/modules/post/pages/index.vue and find // fetch data will be here. That marker is where your API call goes. The generator does not write it, because it does not know your endpoint, your auth or your pagination contract.
Translate
The generator writes the English words into all nine locale files, so nothing renders as a raw key while you work. Replace the eight copies with real translations when you are ready, and run:
npm run check:i18nWhat you get
For the post block above, four files:
src/modules/post/
├── index.ts Registers the command-palette entry
├── pages/index.vue The list page: table, filters, export
└── types/
├── post.types.ts Post, PostId, PostStatus, PostAuthor
└── post.schemas.ts The FieldDefinition listThe types come from the JSON types of your sample values plus the annotations, not from guesswork:
export type PostId = number
export type PostStatus = 'draft' | 'published' | 'archived'
export interface PostAuthor {
id: number
name: string
}
export interface Post extends Record<string, unknown> {
id: PostId
title: string
cover: string
status: PostStatus
excerpt: string | null
views?: number
author: PostAuthor
tags: string[]
published_at: string
}And three edits outside the folder: the side-effect import in src/main.ts, a sidebar entry in src/layouts/navItems.ts with a Blocks icon to swap for the right one, and sixteen translation keys under post_module in every locale file.
What it deliberately leaves to you
Permissions, the API call and its auth, the pagination request contract, and what belongs in the store. Each one is a marked seam in the generated code, not an omission. Add the permission to the page's <route> block when you have decided the name:
{
"path": "/posts",
"name": "posts",
"meta": { "title": "post_module.title", "breadcrumb": "post_module.title", "permission": "read Post" }
}By hand
A module does not have to come from the CLI. The minimum is a folder with a page:
Create the page
src/modules/note/pages/index.vue:
<script setup lang="ts">
import ListPageLayout from '@shared/components/common/pages/ListPageLayout.vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>
<template>
<ListPageLayout :title="t('note_module.title')">
<p>Notes go here.</p>
</ListPageLayout>
</template>
<route lang="json">
{
"path": "/notes",
"name": "notes",
"meta": { "title": "note_module.title", "breadcrumb": "note_module.title" }
}
</route>Add the words
In src/locales/en.json, and in the other eight files:
{
"note_module": {
"title": "Notes"
}
}Add the sidebar entry
In src/layouts/navItems.ts:
{ label: 'Notes', labelKey: 'note_module.title', to: '/notes', icon: NotebookPen }Restart
npm run devYou only need an index.ts and a line in main.ts when the module registers something at startup, such as a command-palette entry.
Removing one
rm -rf src/modules/campaignThen delete its import from src/main.ts, its entry from src/layouts/navItems.ts, and its namespace from the nine locale files. npm run check:i18n tells you if you missed a reference.
Where to go next
- Connect your API — fill in the fetch.
- Schema — what the generated
FieldDefinitionlist can do. - CLI — every flag, and what each refusal means.