# Routes

URL: https://zerodev.ca/docs/medusa/developer-documentation/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

```js title="modules/example/src/routes/items/route.js"
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:

```js
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 the `x-guild-id` header), or the primary `GUILD_ID` for 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 a `404` before the route runs.
* **`request.headers["x-bot"]`**: The bot selected in the dashboard, as `"primary"` or the bot's user id. Use it with `this.medusa.discord.owner(module).id` to only return data for modules that bot runs.

### Sending Responses

Whatever value your method returns is automatically converted to JSON:

```js
async GET(request) {
    return { status: "operational", timestamp: Date.now() };
}
```

If you send headers or stream data manually via `response`, Medusa does not intervene:

```js
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:

```json
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`:

```js title="modules/music/src/routes/activity/route.js"
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

1. **Embedded App SDK**: Toggle **Enable Activities** in the Discord Developer Portal under Embedded App SDK.
2. **URL Mappings**: Under **URL Mappings**, map root prefix `/` to your host domain (e.g. `api.yourdomain.com` or 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.
3. **OAuth2 Redirect**: Add `https://<application-id>.discordsays.com/.proxy/api/modules/<module>/activity` under OAuth2 Redirects.
4. **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.
5. **Channel Invites**: The bot creates an invite using `targetType: 2` (EmbeddedApplication) targeting its own client ID to generate native Discord launch cards.
