GitHub

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:

FileRoute
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
The filename does not need to match the route exactly — it is just a helpful convention. A file named product.js can handle /products/:id without any issue.