Building with File-Based Modules
Learn how to structure your Minima.js application using filesystem-based modules. By the end of this tutorial, you'll understand how to organize features into modules, scope plugins, and build scalable APIs using nothing but your file structure.
What You'll Learn
- Creating your first auto-discovered module
- Adding plugins to modules with
meta.plugins - Building nested module hierarchies
- Setting up global configuration with a root module
- Customizing module discovery behavior
Prerequisites
This tutorial assumes you have a basic Minima.js app with an entry point (src/index.ts). If not, see Getting Started first.
Step 1: Create Your First Module
Let's start by creating a simple users module. Minima.js will automatically discover any file named module.ts in subdirectories.
1. Create the directory structure:
src/
├── index.ts
└── users/
└── module.ts # This will be auto-discovered2. Write your first module:
import type { Routes } from "@minimajs/server";
import { params } from "@minimajs/server";
const users = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
];
function listUsers() {
return users;
}
function getUser() {
const id = params.get("id");
return users.find((u) => u.id === Number(id));
}
export const routes: Routes = {
"GET /list": listUsers,
"GET /:id": getUser,
};import { createApp } from "@minimajs/server/bun";
const app = createApp(); // Auto-discovers users/module.ts
await app.listen({ port: 3000 });3. Test it:
curl http://localhost:3000/users/list
# → [{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]
curl http://localhost:3000/users/1
# → {"id":1,"name":"Alice"}🎉 What just happened?
- Minima.js found
users/module.tsautomatically - The directory name (
users) became the route prefix (/users) - Your routes (
/listand/:id) were mounted under/users
Step 2: Add Plugins to Your Module
Now let's add some plugins to our users module - like request logging and CORS.
1. Add the meta export with plugins:
import type { Meta, Routes } from "@minimajs/server";
import { hook, params } from "@minimajs/server";
import { cors } from "@minimajs/server/plugins";
const users = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
];
// Register plugins in meta.plugins
export const meta: Meta = {
plugins: [
cors(), // Enable cors for this module
hook("request", ({ request }) => {
console.log(`[Users] ${request.method} ${request.url}`);
}),
],
};
function listUsers() {
return users;
}
function getUser() {
const id = params.get("id");
return users.find((u) => u.id === Number(id));
}
export const routes: Routes = {
"GET /list": listUsers,
"GET /:id": getUser,
};2. Test the routes:
curl http://localhost:3000/users/list
# → [{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]
# Check your server logs - you'll see the logging hook output✨ Key Concept: Plugins in meta.plugins are scoped to the module. They only affect routes in this module, not others.
Step 3: Create a Nested Module
Let's add a nested module for user profiles. This demonstrates how modules can be organized hierarchically.
1. Create the nested structure:
src/
├── index.ts
└── users/
├── module.ts
└── profile/
└── module.ts # Nested module2. Create the nested module:
import type { Routes } from "@minimajs/server";
import { params } from "@minimajs/server";
function getProfile() {
const userId = params.get("userId");
return {
userId,
bio: "User profile for " + userId,
settings: { theme: "dark" },
};
}
export const routes: Routes = {
"GET /:userId": getProfile,
};3. Test the nested route:
curl http://localhost:3000/users/profile/1
# → {"userId":"1","bio":"User profile for 1",...}📁 Route Structure:
src/users/module.ts→/users/*src/users/profile/module.ts→/users/profile/*
The prefixes stack automatically!
Step 4: Share Config with a Parent Module
What if you want multiple child modules to share plugins? Use a parent module.
1. Restructure to use a parent:
src/
├── index.ts
└── api/
├── module.ts # Parent module
├── users/
│ └── module.ts # Child 1
└── posts/
└── module.ts # Child 22. Create the parent module with shared plugins:
import type { Meta, Routes } from "@minimajs/server";
import { cors } from "@minimajs/server/plugins";
// These plugins apply to ALL child modules
export const meta: Meta = {
prefix: "/api/v1",
plugins: [cors({ origin: "*" })],
};
function getHealth() {
return { status: "ok" };
}
export const routes: Routes = {
"GET /health": getHealth,
};import type { Routes } from "@minimajs/server";
function listUsers() {
return { users: [] };
}
// No need to register cors - inherited from parent!
export const routes: Routes = {
"GET /list": listUsers,
};import type { Routes } from "@minimajs/server";
function listPosts() {
return { posts: [] };
}
// Also inherits CORS from parent
export const routes: Routes = {
"GET /list": listPosts,
};3. Check the resulting routes:
GET /api/v1/health(parent)GET /api/v1/users/list(child, with inherited plugins)GET /api/v1/posts/list(child, with inherited plugins)
🎯 Inheritance: Child modules automatically get their parent's prefix and plugins!
Step 5: Set Up a Root Module (Global Config)
For truly global configuration that applies to every module, create a root module in your discovery root.
1. Create a root module:
src/
├── index.ts
├── module.ts # ROOT module - applies to everything
├── users/
│ └── module.ts
└── posts/
└── module.ts2. Add global plugins in the root module:
import type { Meta, Routes } from "@minimajs/server";
import { cors } from "@minimajs/server/plugins";
import { hook } from "@minimajs/server";
// 🌍 Global configuration - inherited by ALL modules
export const meta: Meta = {
prefix: "/api",
plugins: [
cors({ origin: "*" }), // All routes get CORS
hook("request", ({ request }) => {
console.log(`[Global] ${request.method} ${request.url}`);
}),
],
};
function getHealth() {
return { status: "ok" };
}
export const routes: Routes = {
"GET /health": getHealth,
};3. Now every module gets these plugins automatically:
import type { Routes } from "@minimajs/server";
function listUsers() {
return { users: [] };
}
// No CORS here - inherited from root!
export const routes: Routes = {
"GET /list": listUsers,
};Resulting structure:
GET /api/health(root)GET /api/users/list(inherits/apiprefix + all plugins)GET /api/posts/list(inherits/apiprefix + all plugins)
💡 Best Practice: Put authentication, CORS, rate limiting, and global logging in the root module.
Common Patterns
Pattern 1: API Versioning
src/
├── module.ts # Root with global auth
├── v1/
│ ├── module.ts # Prefix: /api/v1
│ ├── users/
│ │ └── module.ts
│ └── posts/
│ └── module.ts
└── v2/
├── module.ts # Prefix: /api/v2
└── users/
└── module.tsPattern 2: Public vs Protected Routes
import { authPlugin } from "./plugins/auth.js";
// Root module - makes auth available everywhere
export const meta: Meta = {
plugins: [authPlugin],
};import type { Routes } from "@minimajs/server";
function login() {
/* ... */
}
// No guard - anyone can access
export const routes: Routes = {
"POST /login": login,
};import type { Meta, Routes } from "@minimajs/server";
import { guardPlugin } from "../plugins/guard.js";
// Add guard to require authentication
export const meta: Meta = {
plugins: [guardPlugin],
};
function getProfile() {
/* ... */
}
export const routes: Routes = {
"GET /profile": getProfile,
};Pattern 3: Feature-Based Organization
src/
├── auth/
│ ├── module.ts # Login, logout, etc.
│ └── middleware/
│ └── guard.ts
├── users/
│ ├── module.ts # User CRUD
│ └── profile/
│ └── module.ts # User profiles
└── posts/
├── module.ts # Post CRUD
└── comments/
└── module.ts # Post commentsTroubleshooting
My module isn't being discovered
Check:
- ✅ Is the file named
module.{ts,js}? - ✅ Is it in a subdirectory of your entry point?
- ✅ Is
moduleDiscoveryenabled? (It's on by default)
Debug by logging discovered modules:
const app = createApp();
console.log("Checking module discovery...");
await app.ready();Plugins not working
Remember:
meta.pluginsonly works inmodule.tsfiles (or your configured index filename)- Plugins are scoped to the module and its children
- Parent modules' plugins are inherited by children
Routes returning 404
Check your prefix stacking:
src/api/users/module.ts
└─> /api (from parent) + /users (from directory) = /api/users/*Use absolute prefixes in meta.prefix to override:
export const meta: Meta = {
prefix: "/custom", // Overrides directory-based prefix
};Next Steps
Now that you understand modules, explore:
- Plugins - Create reusable plugins for your modules
- Hooks - Learn all available lifecycle hooks
- JWT Authentication - Build a real auth system with modules
Quick Reference
File Naming
- Default:
module.{ts,js} - Advanced configuration: Module Discovery
Module Structure
import type { Meta, Routes } from "@minimajs/server";
export const meta: Meta = {
prefix: "/custom", // Optional: override directory name
plugins: [
/* ... */
], // Optional: module-scoped plugins
};
export const routes: Routes = {
// Your routes here
};Module Types
- Regular Module: Any
module.tsin a subdirectory - Root Module:
module.tsin the discovery root (global config) - Nested Module:
module.tsinside another module's directory
Plugin Scope
- Root module plugins → Inherited by ALL modules
- Parent module plugins → Inherited by children
- Module plugins → Only that module
- Sibling modules → Isolated from each other