Skip to content

Route Helpers

Rails route helpers (users_path, edit_post_path) don't exist on the frontend. Without them, you hardcode URL strings -- typos are silent, renamed routes break at runtime, and parameter mismatches go unnoticed until production.

Typelizer generates type-safe TypeScript (or JavaScript) route functions from your config/routes.rb. Each controller gets its own module with methods for every action, giving you autocompletion, type-checked parameters, and zero runtime dependencies beyond the generated code.

Enable Route Generation

Add to your initializer:

ruby
# config/initializers/typelizer.rb
Typelizer.configure do |config|
  config.routes.enabled = true
end

Generate Route Helpers

Run the rake task:

bash
# Generate route helpers only
rails typelizer:routes

# Generate both types and routes
rails typelizer:generate

# Clean and regenerate everything
rails typelizer:generate:refresh

Understanding the Generated Output

Given these Rails routes:

ruby
Rails.application.routes.draw do
  root "pages#index"
  resources :users
  resources :posts
end

Typelizer generates three kinds of files in the output directory (default: app/javascript/routes/):

Per-controller files contain route helper methods grouped by controller:

typescript
// routes/UsersController.ts
import type { RouteDefinition, RouteOptions } from './runtime'
import { buildUrl } from './runtime'

export default {
  /** GET /users */
  index: (options?: RouteOptions): RouteDefinition<'get'> => ({
    url: buildUrl('/users', {}, options),
    method: 'get',
  }),

  /** GET /users/:id */
  show: (
    params: { id: string | number } | string | number,
    options?: RouteOptions,
  ): RouteDefinition<'get'> => ({
    url: buildUrl('/users/:id', params, options),
    method: 'get',
  }),

  // ... index, create, new, edit, update, destroy
}

index.ts re-exports all controllers as namespaces and provides shortcuts for named routes:

typescript
// routes/index.ts
export { default as users } from './UsersController'
export { default as posts } from './PostsController'

// Named route shortcuts
export const root = _pages.index
export const newUser = _users.new
export const editPost = _posts.edit
// ...

runtime.ts contains the URL builder and type definitions used by all controller files.

Using Route Helpers

Import by Controller Namespace

Import the controller namespace to access all its routes:

typescript
import { users } from '@/routes'

// GET /users
const list = users.index()
// => { url: "/users", method: "get" }

// GET /users/42
const detail = users.show(42)
// => { url: "/users/42", method: "get" }

Import Named Routes

Import named route shortcuts directly:

typescript
import { editUser, newPost } from '@/routes'

const edit = editUser(42)
// => { url: "/users/42/edit", method: "get" }

Passing Parameters

Routes with a single required parameter accept a value directly:

typescript
users.show(42)
users.show("abc-123")

Routes with multiple parameters require an object:

typescript
import { posts } from '@/routes'

// GET /users/:user_id/posts/:id
posts.userPost({ userId: 1, id: 42 })

Query Strings and Anchors

Pass query and anchor in the options:

typescript
users.index({ query: { page: 2, per: 25 } })
// => { url: "/users?page=2&per=25", method: "get" }

posts.show(42, { anchor: "comments" })
// => { url: "/posts/42#comments", method: "get" }

Optional Parameters

Optional route segments are typed with ?:

typescript
import { posts } from '@/routes'

// GET /archive(/:year)(/:month)
posts.archive({})
// => { url: "/archive", method: "get" }

posts.archive({ year: 2025 })
// => { url: "/archive/2025", method: "get" }

posts.archive({ year: 2025, month: 3 })
// => { url: "/archive/2025/3", method: "get" }

URL Defaults

Set global URL defaults that are merged into every route:

typescript
import { setUrlDefaults, addUrlDefault } from '@/routes/runtime'

// Set all defaults at once
setUrlDefaults({ locale: 'en' })

// Add a single default
addUrlDefault('locale', 'en')

// Dynamic defaults with a function
setUrlDefaults(() => ({
  locale: getCurrentLocale(),
}))

Base URL

By default, route helpers generate relative paths. If your frontend talks to a different host (e.g., a separate API server), set a base URL:

typescript
import { setBaseUrl } from '@/routes/runtime'

setBaseUrl('https://api.example.com')

posts.index()
// => { url: "https://api.example.com/posts", method: "get" }

This is useful when your Rails API and frontend are deployed separately. Set it once at app startup and all route helpers prepend it automatically.

Filtering Routes

Use include and exclude to control which routes are generated:

ruby
Typelizer.configure do |config|
  config.routes.enabled = true

  # Only generate routes matching these patterns
  config.routes.include = [/^\/api/]

  # Skip routes matching these patterns
  config.routes.exclude = [/^\/admin/, /^\/internal/]
end

Both accept a single Regexp or an array of patterns. When include is set, only matching routes are generated. exclude is applied after include.

Filtering with a predicate

include and exclude also accept a Proc (or any object responding to #call) in addition to regexes. The predicate receives a route-info hash and must return truthy to match:

ruby
Typelizer.configure do |config|
  config.routes.include = ->(r) { r[:controller].to_s.start_with?("admin/") }
end

The hash keys available to the predicate are: :path, :name, :controller, :action, :verb, :required_parts, :optional_parts.

You can mix predicates and regexes in an array; routes match if any element matches:

ruby
config.routes.include = [
  /^\/sessions/,
  ->(r) { r[:controller] == "admin/users" }
]

Use predicates when the URL path doesn't uniquely identify the routes you want — for example, when multiple feature areas are mounted under different subdomain constraints but share URL paths (foo.example.com/users and bar.example.com/users both have path /users but different controllers).

Engine Support

Mounted Rails engines are included automatically. Route paths include the mount prefix:

ruby
# config/routes.rb
mount BlogEngine::Engine, at: "/blog"

This generates BlogEngine/ArticlesController.ts with paths like /blog/articles and /blog/articles/:id. The engine's named routes are prefixed with the mount name (e.g., blogEngineArticles).

Auto-Regeneration

When the Listen gem is installed and routes are enabled, Typelizer watches config/ for changes to route files and regenerates automatically.

JavaScript Output

For projects without TypeScript, set the format to :js:

ruby
Typelizer.configure do |config|
  config.routes.enabled = true
  config.routes.format = :js
end

This generates .js files without type annotations. The runtime functions and API are identical.

Namespaced Routes

Namespaced routes are placed in subdirectories matching the namespace:

ruby
namespace :admin do
  resources :users, only: [:index, :show, :destroy]
end

Generates Admin/UsersController.ts with paths like /admin/users and /admin/users/:id.

Import via the namespace:

typescript
import { adminUsers } from '@/routes'

adminUsers.index()
// => { url: "/admin/users", method: "get" }