Skip to content
Back to skills

Fsharp Routing

ASecurity

Implement client-side routing with Feliz.Router for F# Elmish applications. Use when: "add routing", "create pages", "navigation", "URL handling", "deep linking", "add page", "route to", "browser history", "SPA routing", "multi-page", "navigate to". Creates route definitions, URL parsing, and navigation patterns in src/Client/.

  • 666 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added February 8, 2026
developmentgoreactgitfrontenddocumentation

Works with

  • cli

Security analysis

A100/100

Scanned February 12, 2026

npx -y skills add majiayu000/claude-skill-registry --skill fsharp-routing --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Fsharp Routing?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Fsharp Routing
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/majiayu000-fsharp-routing/badge)](https://www.skillsdirectory.com/skills/majiayu000-fsharp-routing)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: fsharp-routing
description: |
  Implement client-side routing with Feliz.Router for F# Elmish applications.
  Use when: "add routing", "create pages", "navigation", "URL handling", "deep linking",
  "add page", "route to", "browser history", "SPA routing", "multi-page", "navigate to".
  Creates route definitions, URL parsing, and navigation patterns in src/Client/.
allowed-tools: Read, Edit, Write, Grep, Glob
---

# F# Client-Side Routing (Feliz.Router)

## When to Use This Skill

Activate when:
- User requests "add routing", "create pages", "add navigation"
- Need to handle URLs in the SPA
- Implementing multi-page application structure
- Adding deep linking support
- User mentions "navigate to", "route to", "URL handling"

## Architecture

```
URL Change
    ↓
Router.currentUrl() - Parse URL segments
    ↓
Route (Discriminated Union)
    ↓
Update function - Set current page
    ↓
View - Render page based on route
```

## Route Definition

### Define Routes as Discriminated Union

```fsharp
// src/Client/Types.fs (or State.fs)
module Types

type Route =
    | Home
    | Items
    | ItemDetail of id: int
    | NewItem
    | Settings
    | NotFound

module Route =
    /// Parse URL segments into Route
    let parse (segments: string list) : Route =
        match segments with
        | [] -> Home
        | [ "items" ] -> Items
        | [ "items"; "new" ] -> NewItem
        | [ "items"; Route.Int id ] -> ItemDetail id
        | [ "settings" ] -> Settings
        | _ -> NotFound

    /// Convert Route to URL path
    let toPath (route: Route) : string =
        match route with
        | Home -> "/"
        | Items -> "/items"
        | ItemDetail id -> $"/items/{id}"
        | NewItem -> "/items/new"
        | Settings -> "/settings"
        | NotFound -> "/not-found"
```

**Key points:**
- Routes are a discriminated union (exhaustive matching)
- `Route.Int` is a Feliz.Router active pattern for parsing integers
- Bidirectional: parse URLs and generate URLs

## State Integration

### Model with Current Route

```fsharp
// src/Client/State.fs
module State

open Elmish
open Feliz.Router
open Types

type Model = {
    CurrentRoute: Route
    Items: RemoteData<Item list>
    SelectedItem: RemoteData<Item>
    // ... other state
}

type Msg =
    | UrlChanged of string list
    | NavigateTo of Route
    | LoadItems
    | ItemsLoaded of Result<Item list, string>
    // ... other messages
```

### Init with Route Parsing

```fsharp
let init () : Model * Cmd<Msg> =
    let initialRoute = Router.currentUrl() |> Route.parse

    let model = {
        CurrentRoute = initialRoute
        Items = NotAsked
        SelectedItem = NotAsked
    }

    // Load data based on initial route
    let cmd =
        match initialRoute with
        | Home | Items -> Cmd.ofMsg LoadItems
        | ItemDetail id -> Cmd.ofMsg (LoadItem id)
        | _ -> Cmd.none

    model, cmd
```

### Update with Navigation

```fsharp
let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =
    match msg with
    | UrlChanged segments ->
        let route = Route.parse segments
        let model = { model with CurrentRoute = route }

        // Trigger data loading for new route
        let cmd =
            match route with
            | Items -> Cmd.ofMsg LoadItems
            | ItemDetail id -> Cmd.ofMsg (LoadItem id)
            | _ -> Cmd.none

        model, cmd

    | NavigateTo route ->
        // Programmatic navigation
        let path = Route.toPath route
        model, Cmd.navigatePath path

    | LoadItems ->
        // ... existing logic
```

## View with Router

### Main App with Router

```fsharp
// src/Client/App.fs
module App

open Feliz
open Feliz.Router
open State
open View

[<ReactComponent>]
let App () =
    let model, dispatch = React.useElmish(init, update, [| |])

    React.router [
        router.onUrlChanged (UrlChanged >> dispatch)
        router.children [
            view model dispatch
        ]
    ]
```

### Page-Based View

```fsharp
// src/Client/View.fs
module View

open Feliz
open Types
open State

let private navbar (dispatch: Msg -> unit) =
    Html.nav [
        prop.className "navbar bg-base-100 shadow-lg"
        prop.children [
            Html.div [
                prop.className "flex-1"
                prop.children [
                    Html.a [
                        prop.className "btn btn-ghost text-xl"
                        prop.href (Router.format [])
                        prop.text "Home"
                    ]
                ]
            ]
            Html.div [
                prop.className "flex-none"
                prop.children [
                    Html.ul [
                        prop.className "menu menu-horizontal px-1"
                        prop.children [
                            Html.li [
                                Html.a [
                                    prop.href (Router.format [ "items" ])
                                    prop.text "Items"
                                ]
                            ]
                            Html.li [
                                Html.a [
                                    prop.href (Router.format [ "settings" ])
                                    prop.text "Settings"
                                ]
                            ]
                        ]
                    ]
                ]
            ]
        ]
    ]

let private homePage (model: Model) (dispatch: Msg -> unit) =
    Html.div [
        prop.className "hero min-h-screen bg-base-200"
        prop.children [
            Html.div [
                prop.className "hero-content text-center"
                prop.children [
                    Html.h1 [ prop.className "text-5xl font-bold"; prop.text "Welcome" ]
                    Html.p [ prop.className "py-6"; prop.text "Your home page content" ]
                    Html.button [
                        prop.className "btn btn-primary"
                        prop.text "View Items"
                        prop.onClick (fun _ -> dispatch (NavigateTo Items))
                    ]
                ]
            ]
        ]
    ]

let private itemsPage (model: Model) (dispatch: Msg -> unit) =
    Html.div [
        prop.className "container mx-auto p-4"
        prop.children [
            Html.h1 [ prop.className "text-3xl font-bold mb-4"; prop.text "Items" ]

            match model.Items with
            | NotAsked -> Html.button [
                prop.className "btn btn-primary"
                prop.text "Load Items"
                prop.onClick (fun _ -> dispatch LoadItems)
              ]
            | Loading -> Html.span [ prop.className "loading loading-spinner" ]
            | Success items ->
                Html.div [
                    prop.className "grid grid-cols-3 gap-4"
                    prop.children [
                        for item in items ->
                            Html.div [
                                prop.className "card bg-base-100 shadow"
                                prop.children [
                                    Html.div [
                                        prop.className "card-body"
                                        prop.children [
                                            Html.h2 [ prop.className "card-title"; prop.text item.Name ]
                                            Html.a [
                                                prop.className "btn btn-sm btn-primary"
                                                prop.href (Router.format [ "items"; string item.Id ])
                                                prop.text "View"
                                            ]
                                        ]
                                    ]
                                ]
                            ]
                    ]
                ]
            | Failure err -> Html.div [ prop.className "alert alert-error"; prop.text err ]
        ]
    ]

let private itemDetailPage (id: int) (model: Model) (dispatch: Msg -> unit) =
    Html.div [
        prop.className "container mx-auto p-4"
        prop.children [
            Html.a [
                prop.className "btn btn-ghost mb-4"
                prop.href (Router.format [ "items" ])
                prop.text "< Back to Items"
            ]

            match model.SelectedItem with
            | NotAsked | Loading -> Html.span [ prop.className "loading loading-spinner" ]
            | Success item ->
                Html.div [
                    Html.h1 [ prop.className "text-3xl font-bold"; prop.text item.Name ]
                    // ... item details
                ]
            | Failure err -> Html.div [ prop.className "alert alert-error"; prop.text err ]
        ]
    ]

let private notFoundPage () =
    Html.div [
        prop.className "hero min-h-screen bg-base-200"
        prop.children [
            Html.div [
                prop.className "hero-content text-center"
                prop.children [
                    Html.h1 [ prop.className "text-5xl font-bold"; prop.text "404" ]
                    Html.p [ prop.className "py-6"; prop.text "Page not found" ]
                    Html.a [
                        prop.className "btn btn-primary"
                        prop.href (Router.format [])
                        prop.text "Go Home"
                    ]
                ]
            ]
        ]
    ]

let view (model: Model) (dispatch: Msg -> unit) =
    Html.div [
        navbar dispatch

        Html.main [
            prop.className "min-h-screen"
            prop.children [
                match model.CurrentRoute with
                | Home -> homePage model dispatch
                | Items -> itemsPage model dispatch
                | ItemDetail id -> itemDetailPage id model dispatch
                | NewItem -> Html.div "New Item Form"
                | Settings -> Html.div "Settings Page"
                | NotFound -> notFoundPage ()
            ]
        ]
    ]
```

## Navigation Patterns

### Link Navigation (Declarative)

```fsharp
// Simple link
Html.a [
    prop.href (Router.format [ "items" ])
    prop.text "Items"
]

// Link with parameters
Html.a [
    prop.href (Router.format [ "items"; string itemId ])
    prop.text "View Item"
]

// Styled as button
Html.a [
    prop.className "btn btn-primary"
    prop.href (Router.format [ "items"; "new" ])
    prop.text "Create Item"
]
```

### Programmatic Navigation

```fsharp
// Via message dispatch
Html.button [
    prop.onClick (fun _ -> dispatch (NavigateTo (ItemDetail 42)))
    prop.text "Go to Item 42"
]

// In update function
| SaveCompleted (Ok item) ->
    model, Cmd.navigatePath (Route.toPath (ItemDetail item.Id))

| DeleteCompleted (Ok _) ->
    model, Cmd.navigatePath "/items"
```

### URL Helpers

```fsharp
// Format URL segments
Router.format [ "items" ]                    // "/items"
Router.format [ "items"; "123" ]             // "/items/123"
Router.format [ "items"; "new" ]             // "/items/new"

// With query parameters
Router.format ([ "items" ], [ "page", "2"; "sort", "name" ])
// "/items?page=2&sort=name"

// Navigate command
Cmd.navigatePath "/items"
Cmd.navigatePath (Router.format [ "items"; string id ])
```

## Active Patterns for Parsing

```fsharp
// Built-in active patterns from Feliz.Router
match segments with
| [ "items"; Route.Int id ] -> ItemDetail id      // Parse int
| [ "users"; Route.Guid guid ] -> UserDetail guid // Parse GUID
| [ "date"; Route.Date d ] -> DateView d          // Parse date

// Custom active pattern
let (|Slug|_|) (s: string) =
    if s.Length > 0 && s |> Seq.forall (fun c -> Char.IsLetterOrDigit c || c = '-')
    then Some s
    else None

match segments with
| [ "posts"; Slug slug ] -> PostBySlug slug
| _ -> NotFound
```

## Query Parameters

```fsharp
// Parse query parameters
let parseWithQuery (segments: string list) (query: Map<string, string>) : Route * QueryParams =
    let route = Route.parse segments
    let page = query |> Map.tryFind "page" |> Option.bind Int32.TryParse |> Option.defaultValue 1
    let sort = query |> Map.tryFind "sort" |> Option.defaultValue "date"
    route, { Page = page; Sort = sort }

// In update
| UrlChanged segments ->
    let query = Router.currentQuery() |> Map.ofList
    let route, params = parseWithQuery segments query
    { model with CurrentRoute = route; QueryParams = params }, Cmd.none
```

## Verification Checklist

- [ ] Route discriminated union defined
- [ ] `Route.parse` handles all URL patterns
- [ ] `Route.toPath` generates correct URLs
- [ ] `React.router` wraps the app
- [ ] `router.onUrlChanged` dispatches to update
- [ ] Init parses initial URL
- [ ] Update handles `UrlChanged` message
- [ ] View renders based on `CurrentRoute`
- [ ] Links use `Router.format`
- [ ] Programmatic navigation uses `Cmd.navigatePath`
- [ ] 404/NotFound route handled

## Common Pitfalls

**Don't:**
- Hardcode URL strings in multiple places
- Forget to handle the initial URL in `init`
- Skip the NotFound case
- Use `window.location` directly

**Do:**
- Define routes as discriminated union
- Use `Router.format` for all URLs
- Handle route changes in update
- Load data based on route
- Provide back navigation

## Related Skills

- **fsharp-frontend** - State and view patterns
- **fsharp-shared** - Types for route parameters
- **fsharp-feature** - Full feature with routing

## Related Documentation

- `/docs/02-FRONTEND-GUIDE.md` - Frontend patterns
- [Feliz.Router Documentation](https://zaid-ajaj.github.io/Feliz/#/Feliz.Router/Overview)

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.