Routing
Every route in Pulse is explicitly declared in the spec. There is no file-based routing, no magic directory conventions, and no implicit mapping. Every page's URL is visible in its spec file and nowhere else.
The route field
Every spec has a route field. This is the URL pattern the spec handles:
export default {
route: '/about',
state: {},
view: () => `<h1>About</h1>`,
}
Pulse matches the exact path. Trailing slashes are normalised — /about and /about/ are treated the same.
Dynamic segments
Use a colon prefix for dynamic path segments. Named segments are captured and available in ctx.params in server data:
export default {
route: '/products/:id',
state: { quantity: 1 },
server: {
data: async (ctx) => {
// ctx.params.id is the captured segment
const product = await db.products.find(ctx.params.id)
return { product }
},
},
view: (state, server) => `<h1>${server.product.name}</h1>`,
}
Multiple dynamic segments
Any number of dynamic segments can appear in a route:
route: '/blog/:year/:month/:slug'
// Matches: /blog/2025/03/my-first-post
// ctx.params = { year: '2025', month: '03', slug: 'my-first-post' }
Registering routes
Specs are registered explicitly by passing them to createServer as an array. Routes are matched in order — more specific routes must come before more general ones:
import { createServer } from '@invisibleloop/pulse'
import home from './src/pages/home.js'
import products from './src/pages/products.js'
import product from './src/pages/product.js' // more specific — comes first
import blog from './src/pages/blog.js'
createServer([home, product, products, blog], { port: 3000 })
Query strings
Query string parameters are not part of the route pattern but are accessible via ctx.query in server data:
// URL: /products?category=shoes&sort=price
server: {
data: async (ctx) => {
const { category, sort } = ctx.query
return { products: await db.products.list({ category, sort }) }
},
}
404 handling
If no spec matches the incoming request path, Pulse returns a 404 response. The response body is a minimal HTML page. To customise the 404 page, use the onError option in createServer:
createServer(specs, {
onError: (err, req, res) => {
if (err.status === 404) {
res.writeHead(404, { 'Content-Type': 'text/html' })
res.end('<h1>Not found</h1>')
}
}
})
File naming conventions
While Pulse does not auto-discover files, the recommended convention maps file names to routes:
| File | Route |
|---|---|
src/pages/home.js | / |
src/pages/about.js | /about |
src/pages/products.js | /products |
src/pages/product.js | /products/:id |
src/pages/blog-post.js | /blog/:slug |
product.js can handle /products/:id without any issue.