DSL & Computation Expressions
NextFs.Dsl is a separate package that provides a high-level F# DSL over NextFs bindings — computation expressions, async helpers, and response shortcuts.
Install
dotnet add package NextFs.Dsl
This pulls in NextFs automatically.
Async Helpers
Eliminate Async.AwaitPromise(Server.xxx()) boilerplate:
open NextFs.Dsl
// Before
let! headers = Async.AwaitPromise(Server.headers())
let! cookies = Async.AwaitPromise(Server.cookies())
// After
let! headers = ServerAsync.headers()
let! cookies = ServerAsync.cookies()
Available in ServerAsync:
headers()— request headerscookies()— cookie storedraftMode()— draft mode stateconnection()— signal request-time dependency
Props unwrapping via PropsAsync:
let! routeParams = PropsAsync.routeParams pageProps
let! searchParams = PropsAsync.searchParams pageProps
let! layoutParams = PropsAsync.layoutParams layoutProps
let! defaultParams = PropsAsync.defaultParams defaultProps
let! contextParams = PropsAsync.contextParams ctx
Route Handlers
The route { } CE wraps async + Async.StartAsPromise and supports let! on both Async<_> and JS.Promise<_>:
open NextFs
open NextFs.Dsl
[<CompiledName("GET")>]
let get (request: NextRequest, ctx: RouteHandlerContext<{| slug: string |}>) =
route {
let! routeParams = PropsAsync.contextParams ctx
return
Route.jsonOk {| slug = routeParams.slug |}
}
[<CompiledName("POST")>]
let post (request: NextRequest) =
route {
let! payload = request.json<obj>()
return Route.json payload 201
}
Response helpers in Route:
| Helper | Description |
|---|---|
Route.jsonOk body | JSON 200 |
Route.json body status | JSON with custom status |
Route.textOk body | Plain text 200 |
Route.text body status | Plain text with custom status |
Route.redirect url | Redirect response |
Route.rewrite url | Rewrite response |
Server Fetch
The fetch url { } CE replaces deeply nested ServerFetchInit.create [ ... ]:
// Before
let! response =
Async.AwaitPromise(
ServerFetch.fetchWithInit "https://api.example.com/posts" (
ServerFetchInit.create [
ServerFetchInit.cache ServerFetchCache.ForceCache
ServerFetchInit.next (
NextFetchOptions.create [
NextFetchOptions.revalidate (Revalidate.seconds 900)
NextFetchOptions.tags [ "posts"; "homepage" ]
]
)
]
)
)
// After
let! response =
fetch "https://api.example.com/posts" {
forceCache
revalidate 900
tags [ "posts"; "homepage" ]
}
Available operations:
| Operation | Description |
|---|---|
noStore | Opt out of caching |
forceCache | Force cache |
cache value | Set cache mode directly |
revalidate seconds | Revalidate interval |
revalidateNever | Revalidate 0 (never cache) |
revalidateForever | Never revalidate |
tags ["a"; "b"] | Cache tags for on-demand revalidation |
tag "a" | Single cache tag |
The CE returns Async<ServerFetchResponse>, so it composes directly with let! in route { } or async { }.
Metadata
The metadata { } CE and its nested builders replace Metadata.create [ ... ]:
let og =
openGraph {
title "My App"
description "App Router in F#"
ogType "website"
}
let tw =
twitter {
card "summary_large_image"
title "My App"
}
let metadata =
metadata {
titleTemplate "My App" "%s | My App"
description "Next.js App Router bindings for F#."
keywords [ "fable"; "nextjs"; "fsharp" ]
openGraph og
twitter tw
}
let viewport =
viewport {
themeColor "#111827"
colorScheme "dark light"
}
Build openGraph and twitter objects outside the metadata { } block to avoid name collision with the custom operations.
Server Components
The serverComponent { } CE wraps async server pages and layouts:
[<ExportDefault>]
let Page (props: PageProps<{| slug: string |}, obj>) =
serverComponent {
let! routeParams = PropsAsync.routeParams props
let! headers = ServerAsync.headers()
let ua = headers.get "user-agent" |> Option.defaultValue "unknown"
return
Html.main [
Html.h1 (sprintf "Page: %s" routeParams.slug)
Html.pre ua
]
}
It supports let! on both Async<_> and JS.Promise<_>, and auto-wraps with Async.StartAsPromise.