Routes
Expose HTTP API endpoints, configure authentication, and handle JSON requests and responses.
Routes expose module operations over HTTP on Medusa's internal API server (port 3001 or SERVER.API.PORT). Medusa automatically registers HTTP routes, handles Bearer token authentication, IP whitelisting, JSON parsing, error status codes, and serialization.
Routes reside in src/routes/ and load from every route.js file located at any directory depth.
Base Class Definition
import { Routes } from "#medusa/modules";
export class ItemsRoute extends Routes {
constructor(medusa) {
super(medusa, {
name: "items",
path: "/items",
authentication: { key: "secret" }
});
}
async GET(request, response) {
const handler = this.medusa.modules.handlers.get(this.module, "items");
const guildId = request.query.guildId;
if (!guildId) return handler.fail(400, "Query parameter 'guildId' is required.");
return handler.listForGuild(guildId);
}
async POST(request, response) {
const handler = this.medusa.modules.handlers.get(this.module, "items");
const { guildId, userId, identifier, name, quantity, price } = request.body;
return handler.create(guildId, userId, identifier, name, { quantity, price });
}
async DELETE(request, response) {
const handler = this.medusa.modules.handlers.get(this.module, "items");
const identifier = request.query.identifier;
if (!identifier) return handler.fail(400, "Query parameter 'identifier' is required.");
return handler.remove(identifier);
}
}Constructor Options
The configuration object passed to super(medusa, meta) accepts the following properties:
| Property | Type | Default | Description |
|---|---|---|---|
name | string | Required | Unique route identifier. |
path | string | Required | Relative path mounted under /api/modules/<module>. |
authentication | object | null | Authentication configuration. When omitted, the route is public. |
Path Namespacing
Medusa automatically namespaces all module routes under /api/modules/<moduleName>:
| Module | Route path | Full Mounted URL |
|---|---|---|
example | /items | /api/modules/example/items |
example | /items/:id | /api/modules/example/items/:id |
ticket | /transcripts | /api/modules/ticket/transcripts |
minecraft | /link/verify | /api/modules/minecraft/link/verify |
Authentication Options
The authentication object supports the following security modes:
authentication: { key: "secret" }
authentication: { key: "api" }
authentication: { key: "secret", ip: true }Authentication Modes
| Mode | Header Requirement | Verification Behavior |
|---|---|---|
{ key: "secret" } | Authorization: Bearer <SECRET> | Compares against AUTHENTICATION.SECRET configured in config.json. Rejects invalid requests with HTTP 401. |
{ key: "api" } | Authorization: Bearer <API_KEY> | Compares against API keys generated in the Medusa dashboard. Records an entry in the system audit log. Rejects invalid keys with HTTP 401. |
{ ip: true } | Client IP address | Restricts access to IP addresses listed in ALLOWED_IPS in the database settings. Rejects unauthorized IPs with HTTP 403. |
null / omitted | None | Publicly accessible route with no authentication check. |
Supported HTTP Methods
Implement any of the following standard HTTP verb methods on your route class:
| Method | Express Route Binding | Typical Use Case |
|---|---|---|
GET(request, response) | app.get(path, handler) | Retrieve records or query state. |
POST(request, response) | app.post(path, handler) | Create records or trigger actions. |
PUT(request, response) | app.put(path, handler) | Replace an entire record or resource. |
DELETE(request, response) | app.delete(path, handler) | Remove records or resources. |
PATCH(request, response) | app.patch(path, handler) | Partially update fields on a record. |
Request and Response Handling
Each method receives standard Express request and response objects:
Reading Input
request.body: Parsed JSON request payload.request.query: Parsed URL query parameters (e.g.?guildId=123).request.params: Route parameters if path uses route parameters (e.g.path: "/items/:id").request.headers: HTTP request headers.request.ip: Client IP address.this.guild(request): The server selected in the dashboard (sent as thex-guild-idheader), or the primaryGUILD_IDfor requests that do not carry one. Scope every database query and config read to it so each server sees only its own data. Dashboard requests to a module that is turned off in the selected server get a404before the route runs.request.headers["x-bot"]: The bot selected in the dashboard, as"primary"or the bot's user id. Use it withthis.medusa.discord.owner(module).idto only return data for modules that bot runs.
Sending Responses
Whatever value your method returns is automatically converted to JSON:
async GET(request) {
return { status: "operational", timestamp: Date.now() };
}If you send headers or stream data manually via response, Medusa does not intervene:
async GET(request, response) {
response.setHeader("Content-Type", "text/plain");
return response.send("Raw text output");
}If a route returns undefined without sending headers, Medusa responds with HTTP 501 Not Implemented.
Error Handling
When any error is thrown inside a route method or called handler (such as this.fail(status, message)), Medusa catches the error and responds:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": "Query parameter 'guildId' is required."
}If the error does not have an attached status property, Medusa defaults to HTTP 500 Internal Server Error.
Discord Activities and Embedded Apps
Public routes can serve embedded applications running inside Discord voice channels using the Discord Embedded App SDK.
Serving On-Demand React Bundles
To bundle React applications on demand without separate build steps, routes can bundle TSX resources using esbuild:
import { Routes } from "#medusa/modules";
import fs from "fs";
import path from "path";
import { fileURLToPath, pathToFileURL } from "url";
export class ActivityRoute extends Routes {
constructor(medusa) {
super(medusa, {
name: "activity",
path: "/activity"
});
this.cachedStamp = 0;
this.cachedBundle = null;
}
async bundle() {
const file = path.join(path.dirname(fileURLToPath(import.meta.url)), "../../../resources/activity/app.tsx");
const stamp = fs.existsSync(file) ? Math.floor(fs.statSync(file).mtimeMs) : 0;
if (this.cachedBundle && this.cachedStamp === stamp) return this.cachedBundle;
const dashboardFolder = this.medusa.dashboard ? this.medusa.dashboard.folder : path.resolve("src/dashboard");
const esbuildPath = path.join(dashboardFolder, "node_modules", "esbuild", "lib", "main.js");
const { build } = await import(pathToFileURL(esbuildPath).href);
const result = await build({
entryPoints: [file],
bundle: true,
write: false,
format: "esm",
target: "es2022",
minify: true,
nodePaths: [path.join(dashboardFolder, "node_modules")],
define: { "process.env.NODE_ENV": JSON.stringify("production") }
});
this.cachedStamp = stamp;
this.cachedBundle = result.outputFiles[0].text;
return this.cachedBundle;
}
async GET(request, response) {
if (request.query && request.query.bundle === "1") {
response.setHeader("Content-Type", "text/javascript; charset=utf-8");
response.setHeader("Cache-Control", "no-cache");
response.send(await this.bundle());
return;
}
response.setHeader("Content-Type", "text/html; charset=utf-8");
response.send(this.loadHtml());
}
}Discord Developer Portal Activity Configuration
- Embedded App SDK: Toggle Enable Activities in the Discord Developer Portal under Embedded App SDK.
- URL Mappings: Under URL Mappings, map root prefix
/to your host domain (e.g.api.yourdomain.comor your tunnel host). The Target field only accepts a hostname; do not include a path segment like/api/..., as Discord will forward the request path directly to the target. - OAuth2 Redirect: Add
https://<application-id>.discordsays.com/.proxy/api/modules/<module>/activityunder OAuth2 Redirects. - Primary Entry Point Command: Register a Primary Entry Point command (
type: "primary_entry_point",handler: 2) so the Activity appears directly in Discord's App Launcher, voice channel activities list, and user profiles. - Channel Invites: The bot creates an invite using
targetType: 2(EmbeddedApplication) targeting its own client ID to generate native Discord launch cards.