| title | Server & Client Patterns |
|---|---|
| layout | default |
| nav_order | 4 |
This guide collects the App Router patterns that usually matter first when moving a Next.js app from JavaScript or TypeScript to F#.
The goal is not to hide Next.js. The goal is to express the same shapes through NextFs bindings with as little translation noise as possible.
If an entry module uses client hooks such as NavigationClient.useRouter() or NavigationClient.usePathname(), keep the F# file focused on the component and generate a thin wrapper with 'use client'.
module App.Page
open Fable.Core
open Feliz
open NextFs
[<ExportDefault>]
[<ReactComponent>]
let Page() =
let pathname = NavigationClient.usePathname()
Html.main [
Html.h1 "Client page"
Html.code pathname
]Wrapper entry:
{
"directive": "use client",
"from": "./.fable/App/Page.js",
"to": "./app/page.js",
"defaultFromNamed": "Page"
}Use Directive.useServer() inside the function body when the server action is defined inline inside a server component, layout, or dedicated server module.
let savePost (formData: FormDataCollection) =
Directive.useServer()
let title =
match formData.get("title") with
| Some (:? string as value) -> value
| _ -> "Untitled"
title |> ignoreIf you want a dedicated action module, export named functions and generate a 'use server' wrapper.
module App.Actions
open NextFs
let createPost (_formData: FormDataCollection) =
Directive.useServer()
()Wrapper entry:
{
"directive": "use server",
"from": "./.fable/App/Actions.js",
"to": "./app/actions.js",
"named": ["createPost"]
}use server wrappers must stay on named exports only. NextFs rejects default exports and export * for that case.
Route handlers use JavaScript-shaped arguments and CompiledName for HTTP verbs.
module App.Api.Posts
open Fable.Core
open Fable.Core.JsInterop
open NextFs
[<CompiledName("POST")>]
let post (request: NextRequest) =
async {
let! payload = Async.AwaitPromise(request.json<obj>())
return
ServerResponse.jsonWithInit
(createObj [ "received" ==> payload ])
(ResponseInit.create [ ResponseInit.status 201 ])
}
|> Async.StartAsPromiseNextFs exposes constructors for NextRequest and NextResponse, which is useful for proxy-style code, tests, and lower-level interop.
open Fable.Core.JsInterop
open NextFs
let request =
ServerRequest.createWithInit "https://example.com/dashboard" (
NextRequestInit.create [
NextRequestInit.method' "POST"
NextRequestInit.body "name=nextfs"
NextRequestInit.nextConfig (
NextConfig.create [
NextConfig.basePath "/docs"
NextConfig.trailingSlash false
]
)
NextRequestInit.duplexHalf
]
)
let response =
ServerResponse.createWithInit
(box """{"ok":true}""")
(ResponseInit.create [
ResponseInit.status 202
ResponseInit.url request.url
])Use NavigationClient.useServerInsertedHTML() when you need the App Router server-inserted HTML hook, for example with CSS-in-JS registries.
[<ReactComponent>]
let StyleRegistry() =
NavigationClient.useServerInsertedHTML(fun () ->
Html.style [
prop.text ".nextfs-registry{display:block;}"
])
Html.div [
prop.className "nextfs-registry"
prop.hidden true
]NavigationClient.unstableIsUnrecognizedActionError() is useful when a client action call fails because the browser is talking to a different deployment than the server.
let isDeploymentMismatch(error: obj) =
NavigationClient.unstableIsUnrecognizedActionError errorThe App Router segment hooks are available with and without parallelRouteKey.
let segment = NavigationClient.useSelectedLayoutSegment()
let analyticsSegment = NavigationClient.useSelectedLayoutSegmentFor "analytics"
let children = NavigationClient.useSelectedLayoutSegmentsFor "children"If you need the computed <img> props instead of the Image component directly, use Image.getImageProps().
open Fable.Core.JsInterop
let heroImage =
Image.getImageProps (
createObj [
"src" ==> "/hero.png"
"alt" ==> "Hero"
"width" ==> 1280
"height" ==> 720
]
)- wrapper rules: Directives and wrappers
- App Router conventions: Special files
- project structure reference: Starter example
- lookup table: API reference