-
Notifications
You must be signed in to change notification settings - Fork 0
Module Structure
Full reference for the file layout, naming rules, and the AppModule contract.
backend/src/modules/<name>/
├── index.ts required — backend entry point
└── public/ optional — static frontend files
├── index.html
├── app.js
└── style.css
Any files in public/ are automatically served at /<name>-assets/. You don't need to configure anything.
Every module's index.ts must export a default object implementing AppModule:
import type { AppModule } from '../../shared/types/module'
const MyModule: AppModule = {
name: 'my-module', // display name (used in logs)
version: '1.0.0', // semver string
async register(server, services, prefix) {
// server — the Fastify instance
// services — CoreServices (db, timer, events, notify, scheduler, time)
// prefix — your module's URL base, e.g. "/my-module"
}
}
export default MyModuleThat's the entire contract. Everything else is up to you.
prefix is derived automatically from your folder name:
| Folder | Prefix |
|---|---|
src/modules/dashboard/ |
/dashboard |
src/modules/kanban/ |
/kanban |
src/modules/my-crm/ |
/my-crm |
Use it to namespace all your routes so they don't collide with other modules:
server.get(`${prefix}/api/items`, ...) // GET /kanban/api/items
server.post(`${prefix}/api/items`, ...) // POST /kanban/api/items
server.delete(`${prefix}/api/items/:id`, ...) // DELETE /kanban/api/items/:idimport path from 'path'
import fs from 'fs'
const publicDir = path.join(process.cwd(), 'src', 'modules', 'my-module', 'public')
const assetPrefix = `${prefix}-assets` // e.g. "/my-module-assets"
server.get(prefix, { config: { public: true } } as never, async (_req, reply) => {
const html = fs.readFileSync(path.join(publicDir, 'index.html'), 'utf-8')
.replaceAll('{{ASSETS}}', assetPrefix)
reply.type('text/html').send(html)
})The {{ASSETS}} token in your HTML gets replaced with the correct asset path at serve time.
By default all routes require JWT authentication. Mark a route as public with { config: { public: true } }:
// Public — no JWT needed
server.get(`${prefix}/api/data`, { config: { public: true } } as never, async () => {
return { ok: true }
})
// Protected — requires Authorization: Bearer <token>
server.get(`${prefix}/api/private`, async (req) => {
return { user: req.user }
})server.get(`${prefix}/api/items`, { config: { public: true } } as never, async () => {
return services.db.item.findMany()
})server.get<{ Params: { id: string } }>(
`${prefix}/api/items/:id`,
{ config: { public: true } } as never,
async (req, reply) => {
const item = await services.db.item.findUnique({ where: { id: req.params.id } })
if (!item) return reply.code(404).send({ error: 'Not found' })
return item
}
)server.post<{ Body: { title: string; column: string } }>(
`${prefix}/api/items`,
{ config: { public: true } } as never,
async (req, reply) => {
if (!req.body.title) return reply.code(400).send({ error: 'title required' })
return services.db.item.create({ data: req.body })
}
)The plugin loader runs at startup and scans src/modules/*/index.ts. It:
- Derives
prefixfrom the folder name - Calls
mod.register(server, services, prefix) - Logs the result
You never need to register a module manually. Drop the folder in, restart.
- Creating-a-Module — Walkthrough from scratch
- Routing-and-Assets — How the URL and asset system works in detail
-
Core-Services — What's in the
servicesobject