Skip to Content

Routing

Every server in Morphis has a routes file in src/routes/ that acts as the single entry point for requests. For the default api server that file is src/routes/api.ts.

src/routes/api.ts
import { Cors, Get, Logger, Router, Track } from 'morphis';
import postController from '../controllers/PostController';
 
const router = new Router();
 
// ── Inline handler ───────────────────────────────────────────────────────────
router.get(() => ({ message: 'OK' }), [Get('/')]);
 
// ── Controller resources ─────────────────────────────────────────────────────
router.resources(postController);
 
// ── Global middleware ────────────────────────────────────────────────────────
router.use([
    Logger,
    Track,
    Cors({ origins: '*' }),
]);
 
export default router;

The router instance is passed to Bun.serve({ fetch: router.handle.bind(router) }) at build time. You never write that boilerplate — it is injected by morphis build.

Binding controller methods

Use router.get / router.post / router.put / router.delete / router.patch to register a single controller method. The path and HTTP verb are read from the @Get / @Post / … decorator attached to that method:

// registers GET /posts/:id → postController.get
router.get(postController.get);
 
// registers POST /posts → postController.create
router.post(postController.create);

You can also pass an inline anonymous handler together with an explicit method middleware:

router.get(() => ({ ok: true }), [Get('/health')]);

router.resources(instance)

For a controller that follows the standard CRUD naming convention, pass the instance once and all five routes are registered automatically:

router.resources(postController);
MethodPathController method
GET/postslist
GET/posts/:idget
POST/postscreate
PUT/posts/:idupdate
DELETE/posts/:iddelete

The base path is derived from the @Controller decorator on the class (e.g. @Controller('/posts')). Any method that does not exist on the instance is silently skipped, so you can implement only the verbs you need.

router.use() — global middleware

router.use() registers middleware that wraps every request handled by the router. Pass a single instance or an array:

router.use([
    Logger,        // log request & response
    Track,         // attach trace-id header
    Cors({ origins: '*' }),   // add CORS headers / handle preflight
    Connect('default'),       // resolve a DB connection for every request
]);

Middlewares run in the order they are added (outermost first), so Logger receives the raw request before Track, and so on.

Register global middleware after all route declarations so the order in the file reads top-to-bottom: routes first, cross-cutting concerns last.

HTTP decorators (controller side)

Decorators are placed on controller methods to declare their path and verb. The router reads this metadata when you call router.get(instance.method) or router.resources(instance).

DecoratorVerb
@Get(path)GET
@Post(path)POST
@Put(path)PUT
@Delete(path)DELETE
@Patch(path)PATCH
@Controller(basePath)(sets base for all methods)
import { Controller, Get, Post, Put, Delete } from 'morphis';
 
@Controller('/posts')
export class PostController {
  @Get('/')
  list() { return [] }
 
  @Get('/:id')
  get() { return {} }
 
  @Post('/')
  create() { return { created: true } }
 
  @Put('/:id')
  update() { return { updated: true } }
 
  @Delete('/:id')
  delete() { return { deleted: true } }
}

Route inspection

List all registered routes and their middleware at any time:

morphis route:list --server=api --format=table

Each row shows the HTTP method, resolved path, action name (controller.method), and any validation or transform middleware attached to the route.

Last updated on