Skip to main content

overview

Psychic follows a paradigm set up by many other MVC frameworks, which is to have a centralized file from which all of your application's routes can be derived. Though some frameworks have done away with such mechanisms, favoring implicit routing built into the application's controllers, Psychic prefers the centralized routing system, since it gives the engineer a high-level overview of their application, and enables them to easily switch routes on and off.

The route bindings for a Psychic application can be found in the conf/routes.ts file. Routes are expressed elegantly using standard HTTP-verb driven language, like so:

import { PsychicRouter } from '@rvoh/psychic'

export default (r: PsychicRouter) => {
r.get('/health-check')
}

GET     /health-check  HealthCheck#ping

RESTful routing

The RESTful verbs (GET, POST, PATCH, PUT, and DELETE) are the backbone of expressive routing in a web application.

Here is an example, demonstrating how these verbs might be used to clearly express a resourcefully-driven application:

import { PsychicRouter } from '@rvoh/psychic'

export default (r: PsychicRouter) => {
r.post('places', PlacesController, 'create')
r.get('places', PlacesController, 'index')
r.get('places/:id', PlacesController, 'show')
r.patch('places/:id', PlacesController, 'update')
r.put('places/:id', PlacesController, 'update')
r.delete('places/:id', PlacesController, 'destroy')
}

POST     /places      Places#create
GET      /places      Places#index
GET      /places/:id  Places#show
PATCH    /places/:id  Places#update
PUT      /places/:id  Places#update
DELETE   /places/:id  Places#destroy

To learn more about REST-driven routing mechanisms, see the REST guide.

Resourceful routing

Since this type of routing pattern is quite common, Psychic provides encapsulated methods which will automatically build out these exact route directives in a single call, using the resources method, like so:

import { PsychicRouter } from '@rvoh/psychic'

export default (r: PsychicRouter) => {
r.resources('places')
}

POST     /places      Places#create
GET      /places      Places#index
GET      /places/:id  Places#show
PATCH    /places/:id  Places#update
PUT      /places/:id  Places#update
DELETE   /places/:id  Places#destroy

To learn more about resourceful routing, see the resourceful routing guide.

Namespacing

The Psychic router also provides techniques for namespacing your application, using nested callbacks to organize clusters of routes together, like so:

import { PsychicRouter } from '@rvoh/psychic'

export default (r: PsychicRouter) => {
r.namespace('v1', r => {
r.namespace('host', r => {
r.resources('places')
})
})
}

POST     /v1/host/places      V1/Host/Places#create
GET      /v1/host/places      V1/Host/Places#index
GET      /v1/host/places/:id  V1/Host/Places#show
PATCH    /v1/host/places/:id  V1/Host/Places#update
PUT      /v1/host/places/:id  V1/Host/Places#update
DELETE   /v1/host/places/:id  V1/Host/Places#destroy

To learn more about techniques for namespacing, see the namespacing guide.

Namespacing and authentication

A namespace's top-level segment isn't just a URL grouping — it lines up with the controller directory tree, which is Psychic's authentication architecture (see controller overview — authentication architecture). Authed client endpoints live under v1/.... Any surface that loosens authentication — public/maybe-authed reads, external webhooks, a server-to-server partner API — gets its own top-level namespace, with its version nested inside, never nested a level down inside v1/:

import { PsychicRouter } from '@rvoh/psychic'

export default function routes(r: PsychicRouter) {
// Authed client API — everything under v1/ is authenticated.
r.namespace('v1', r => {
r.namespace('host', r => {
r.resources('places')
})
})

// A surface that LOOSENS auth is its OWN top-level namespace, version nested inside —
// never under v1/. The directory tree is the auth architecture.
r.namespace('webhooks', r => { // unauthed external callbacks: /webhooks/v1/zoom
r.namespace('v1', r => {
r.post('zoom', WebhooksV1ZoomController, 'create')
})
})

r.namespace('api', r => { // server-to-server partner API: /api/v1/reservations
r.namespace('v1', r => {
r.resources('reservations', { only: ['index', 'show', 'create'] })
})
})
}

v1/webhooks/... would bury an auth change deep inside the authed client branch, which is exactly what the directory-based auth model is meant to prevent. The maybe-authed Visitor surface follows the same rule — it's top-level too, and can still map to a clean /v1/... URL via an explicit controller: reference; see Keeping an auth-context directory out of the URL.