Resource Routes
A directory with route.ts but no page.tsx becomes a resource route — it returns JSON directly, no HTML rendering. Resource routes let you build API endpoints alongside your pages using the same file-based routing conventions.
import type { LoaderContext } from '@paretojs/core'
export function loader(_ctx: LoaderContext) { return { timestamp: new Date().toISOString(), message: 'This is a resource route — no HTML, just JSON.', }}GET /api/time returns:
{ "timestamp": "2026-03-26T12:00:00.000Z", "message": "This is a resource route — no HTML, just JSON."}HTTP methods
Section titled “HTTP methods”- GET → calls the
loaderexport - POST / PUT / PATCH / DELETE → calls the
actionexport
export function loader(ctx: LoaderContext) { return { users: getAllUsers() }}
export async function action(ctx: LoaderContext) { const body = ctx.req.body const user = await createUser(body) return { user }}Response headers and status codes
Section titled “Response headers and status codes”The ctx.res object is a standard Express response. Set custom headers and status codes before returning data:
export function loader(ctx: LoaderContext) { ctx.res.setHeader('Cache-Control', 's-maxage=60, stale-while-revalidate') ctx.res.setHeader('X-Custom-Header', 'my-value') return { data: getCachedData() }}
export async function action(ctx: LoaderContext) { const item = await createItem(ctx.req.body) ctx.res.status(201) return { item }}Error handling in resource routes
Section titled “Error handling in resource routes”Throw errors in resource routes the same way you would in page loaders. Pareto catches the error and returns a JSON error response:
export async function loader(ctx: LoaderContext) { const user = await getUser(ctx.params.id) if (!user) { ctx.res.status(404) return { error: 'User not found' } } return { user }}For structured error responses, you can set the status code explicitly. Unlike page routes, resource routes do not render error boundaries — they return JSON to the caller.
For auth failures, return a JSON error response rather than redirect() — API callers are typically fetch(), not direct browser navigation:
export async function loader(ctx: LoaderContext) { if (!ctx.req.cookies.token) { ctx.res.status(401) return { error: 'Unauthorized' } } const resource = await getResource(ctx.params.id) if (!resource) { ctx.res.status(404) return { error: 'Not found' } } return { resource }}Authentication example
Section titled “Authentication example”A common pattern is to check authentication in resource routes before returning data:
import type { LoaderContext } from '@paretojs/core'
export async function loader(ctx: LoaderContext) { const token = ctx.req.cookies.token if (!token) { ctx.res.status(401) return { error: 'Unauthorized' } }
const user = await verifyToken(token) if (!user) { ctx.res.status(403) return { error: 'Invalid token' } }
return { user }}
export async function action(ctx: LoaderContext) { const token = ctx.req.cookies.token if (!token) { ctx.res.status(401) return { error: 'Unauthorized' } }
const user = await verifyToken(token) if (!user) { ctx.res.status(403) return { error: 'Invalid token' } }
const updated = await updateProfile(user.id, ctx.req.body) return { user: updated }}Middleware patterns
Section titled “Middleware patterns”For shared logic across multiple resource routes (auth checks, logging, rate limiting), apply Express middleware in your custom server (app.ts):
import express from 'express'import { securityHeaders } from '@paretojs/core/node'
const app = express()app.use(securityHeaders())
// Auth middleware for all /api/* routesapp.use('/api', (req, res, next) => { const token = req.cookies.token if (!token) { res.status(401).json({ error: 'Unauthorized' }) return } next()})
export default appYour custom routes and middleware take priority. Unmatched requests fall through to Pareto’s routing automatically. See @paretojs/core/node for details on the custom server pattern.
Related
Section titled “Related”- Configuration —
pareto.config.tsoptions and customizing Vite viavite.config.ts. - File-Based Routing — How
route.tsfits into the file-based routing system. - Redirect & 404 — Using
redirect()andnotFound()in page loaders.