Routing & Controllers
Routes in Hypersonic are standard Express handlers registered on app.express.
Registering routes
// server.ts
const app = await createApp({ config, env, prisma })
app.express.get('/', (_req, res) => {
res.inertia!('Welcome', { message: 'Hello!' })
})All routes must be registered after createApp and before app.start().
Rendering Inertia pages
res.inertia!(component, props) renders a React page component and passes props to it. The component name maps to a file under resources/js/Pages/:
| Call | File resolved |
|---|---|
res.inertia!('Welcome', {}) | resources/js/Pages/Welcome.tsx |
res.inertia!('Auth/Login', {}) | resources/js/Pages/Auth/Login.tsx |
res.inertia!('Posts/Show', { post }) | resources/js/Pages/Posts/Show.tsx |
Props are serialised to JSON, so they must be plain objects. Type them with an interface on the React side — see Frontend.
Protecting routes
createAuthGuard returns a middleware that checks for a valid Better Auth session. Unauthenticated requests are redirected to /login.
// src/middleware.ts (generated by hypersonic new)
import { createAuthGuard } from './src/middleware.ts'
const requireAuth = createAuthGuard(app.auth)
app.express.get('/dashboard', requireAuth, (req, res) => {
res.inertia!('Dashboard', { user: req.sessionUser })
})On a successful session check, the middleware attaches the session user to req.sessionUser.
req.sessionUser
req.sessionUser is typed as SessionUser and is only present on routes protected by createAuthGuard:
app.express.get('/profile', requireAuth, (req, res) => {
const { id, name, email } = req.sessionUser!
res.inertia!('Profile', { id, name, email })
})Error classes
Import these from @hypersonic-js/complete and throw them from any route handler. The Inertia error handler will redirect back with an error state; non-Inertia requests receive a JSON response.
| Class | Status |
|---|---|
NotFoundError | 404 |
UnauthorizedError | 401 |
ForbiddenError | 403 |
ValidationError | 422 |
HttpError(status, message) | custom |
import { NotFoundError, ForbiddenError } from '@hypersonic-js/complete'
app.express.get('/posts/:id', requireAuth, async (req, res, next) => {
try {
const post = await prisma.post.findUnique({ where: { id: Number(req.params.id) } })
if (!post) return next(new NotFoundError('Post not found'))
if (post.userId !== req.sessionUser!.id) return next(new ForbiddenError())
res.inertia!('Posts/Show', { post })
} catch (err) {
next(err)
}
})Always wrap async handlers in try/catch and pass errors to next.
Error handler
Mount createInertiaErrorHandler() after all routes. It intercepts HttpError instances and, for Inertia requests, redirects back instead of breaking the page.
import { createInertiaErrorHandler } from '@hypersonic-js/complete'
// ... all routes above ...
app.express.use(createInertiaErrorHandler())
await app.start()Organising routes
For anything beyond a handful of routes, extract them into src/routes.ts:
// src/routes.ts
import type { Application } from 'express'
import { NotFoundError } from '@hypersonic-js/complete'
import { createAuthGuard } from './middleware.ts'
import type { AuthLike } from './types.ts'
export function registerRoutes(app: Application, prisma: PrismaClient, auth: AuthLike): void {
const requireAuth = createAuthGuard(auth)
app.get('/', (_req, res) => {
res.inertia!('Welcome', {})
})
app.get('/dashboard', requireAuth, (req, res) => {
res.inertia!('Dashboard', { user: req.sessionUser })
})
}// server.ts
import { registerRoutes } from './src/routes.ts'
const app = await createApp({ config, env, prisma })
registerRoutes(app.express, prisma, app.auth)
await app.start()