NextFs is a thin Fable binding layer for writing Next.js App Router applications in F#.
The package stays close to native Next.js concepts instead of introducing a separate framework. If you already understand how a feature works in Next.js, the goal is that you can express the same shape in F# with minimal translation.
If you are evaluating the repository for the first time, use this path:
- read Quickstart
- read Starter app walkthrough
- inspect the starter example
- learn the wrapper rules in Directives and wrappers
- use Data fetching and route config for server
fetch()and route segment exports - use Server and client patterns for route handlers, wrappers, and mixed App Router flows
- use Special files for
error.js,loading.js,not-found.js, and auth-interrupt conventions - use API reference as the lookup table
next/link,next/image,next/script,next/form,next/headnext/font/localand a generatednext/font/googlecatalog- App Router hooks and helpers from
next/navigation - request helpers from
next/headers NextRequest,NextResponse, and route handler helpers fromnext/serverNextRequest/NextResponseconstructors and init builders- typed server
fetch()helpers forcache,next.revalidate, andnext.tags proxy.jsconfig builders andNextFetchEvent- root instrumentation entry patterns for
instrumentation.jsandinstrumentation-client.js - App Router special-file helpers and documented patterns for
error.js,global-error.js,loading.js,not-found.js,global-not-found.js,template.js,default.js,forbidden.js, andunauthorized.js - cache invalidation and cache directives from
next/cache - metadata, viewport, robots, sitemap, manifest, and image-metadata builders
ImageResponsebindings for Open Graph and icon generation- request/response cookie option builders
NavigationClient.useServerInsertedHTML,NavigationClient.unstableIsUnrecognizedActionError,LinkClient.useLinkStatus,WebVitals.useReportWebVitals,after,userAgent,forbidden, andunauthorizedImage.getImageProps()and action-mismatch detection for client-side server-action calls- inline
Directive.useServer()andDirective.useCache()support - wrapper generation for file-level
'use client'and'use server'
NextFs:0.9.xnext:>= 15.0.0 < 17.0.0react:>= 18.2.0 < 20.0.0react-dom:>= 18.2.0 < 20.0.0- core Fable dependencies in this repo:
Fable.Core 4.5.0,Feliz 2.9.0
dotnet add package NextFsYour consuming Next.js app still provides the JavaScript runtime packages:
nextreactreact-dom
NextFs publishes Femto metadata for those packages, so a Fable consumer can check or resolve them with:
dotnet femto yourProject.fsproj
dotnet femto --resolve yourProject.fsprojFor repository work, restore local tools first:
dotnet tool restoreThe repo-local tool manifest currently includes femto and fable.
module App.Page
open Fable.Core
open Feliz
open NextFs
[<ReactComponent>]
let NavLink() =
Link.create [
Link.href "/dashboard"
prop.text "Dashboard"
]
[<ExportDefault>]
let Page() =
Html.main [
prop.children [
Html.h1 "Hello from Fable + Next.js"
NavLink()
]
]Route handlers use JavaScript-shaped arguments and HTTP verb exports:
module App.Api.Posts
open Fable.Core
open Fable.Core.JsInterop
open NextFs
[<CompiledName("GET")>]
let get (request: NextRequest, ctx: RouteHandlerContext<{| slug: string |}>) =
async {
let! routeParams = Async.AwaitPromise ctx.``params``
return
ServerResponse.jsonWithInit
(createObj [
"slug" ==> routeParams.slug
"pathname" ==> request.nextUrl.pathname
])
(ResponseInit.create [
ResponseInit.status 200
])
}
|> Async.StartAsPromiseServer-side helpers follow the asynchronous shape of modern Next.js APIs:
module App.ServerPage
open Feliz
open NextFs
[<ExportDefault>]
let Page() =
async {
let! headers = Async.AwaitPromise(Server.headers())
let userAgent = headers.get("user-agent") |> Option.defaultValue "unknown"
return Html.pre userAgent
}
|> Async.StartAsPromiseNextFs now includes a baseline next/cache surface for App Router workflows:
Directive.useCache()Directive.useCachePrivate()Directive.useCacheRemote()Cache.cacheLifeProfileCache.cacheLifeCache.cacheTag/Cache.cacheTagsCache.revalidatePathCache.revalidateTagCache.updateTagCache.refreshCache.noStore
Example:
let loadNavigationLabels () =
Directive.useCache()
Cache.cacheLifeProfile CacheProfile.Hours
Cache.cacheTags [ "navigation"; "searches" ]
[| "Home"; "Search"; "Docs" |]
let saveSearch (_formData: obj) =
Directive.useServer()
Cache.updateTag "searches"
Cache.revalidatePath "/"
Cache.refresh()
()NextFs now covers the parts of App Router you need to export from layouts and metadata files:
MetadataMetadataOpenGraphMetadataTwitterViewportMetadataRoute.RobotsMetadataRoute.SitemapEntryMetadataRoute.ManifestImageMetadataImageResponse
Example:
let metadata =
Metadata.create [
Metadata.titleTemplate (
MetadataTitle.create [
MetadataTitle.defaultValue "NextFs"
MetadataTitle.template "%s | NextFs"
]
)
Metadata.description "Next.js App Router bindings for F#."
Metadata.openGraph (
MetadataOpenGraph.create [
MetadataOpenGraph.title "NextFs"
MetadataOpenGraph.type' "website"
]
)
]
let viewport =
Viewport.create [
Viewport.themeColor "#111827"
Viewport.colorScheme "dark light"
]For generated icon or Open Graph routes:
let generateImageMetadata() =
[|
ImageMetadata.create [
ImageMetadata.id "small"
ImageMetadata.contentType "image/png"
]
|]The package now also covers the main App Router surfaces that typically force people back to handwritten JavaScript:
Font.localGoogleFont.Inter,GoogleFont.Roboto, and the rest of the generatednext/font/googlecatalogFontOptions,LocalFontSource, andFontDeclarationProxyConfig,ProxyMatcher,RouteHas, andNextFetchEventCookieOptionsfor request/response cookie mutation
Example:
let inter =
GoogleFont.Inter(
box {|
subsets = [| "latin" |]
display = "swap"
variable = "--font-inter"
|}
)
For production App Router builds, keep `next/font` options as anonymous-record or object-literal expressions in the entry module. Next.js statically analyzes these calls and rejects helper-built objects.
let config =
ProxyConfig.create [
ProxyConfig.matcher "/dashboard/:path*"
]Beyond the baseline router/navigation APIs, NextFs now also includes:
LinkClient.useLinkStatus()WebVitals.useReportWebVitals(...)NavigationClient.useServerInsertedHTML(...)NavigationClient.useSelectedLayoutSegmentFor(...)NavigationClient.useSelectedLayoutSegmentsFor(...)NavigationClient.unstableIsUnrecognizedActionError(...)Navigation.forbidden()Navigation.unauthorized()Navigation.unstableRethrow(...)Server.after(...)Server.userAgent(...)ServerFetch.fetch(...)ServerFetch.fetchWithInit(...)ServerRequest.createWithInit(...)ServerResponse.createWithInit(...)Image.getImageProps(...)
These helpers are compile-smoked in samples/NextFs.Smoke.
NextFs now includes typed helpers for Next.js server fetch() options and route segment exports:
ServerFetch.fetch(...)ServerFetch.fetchWithInit(...)ServerFetchInitNextFetchOptionsServerFetchCacheRevalidate.seconds,Revalidate.forever,Revalidate.neverCacheRouteRuntimePreferredRegionGenerateSitemapsEntry
Example:
let runtime = RouteRuntime.Edge
let preferredRegion = PreferredRegion.home
let maxDuration = 30
let loadPosts() =
ServerFetch.fetchWithInit "https://example.com/api/posts" (
ServerFetchInit.create [
ServerFetchInit.cache ServerFetchCache.ForceCache
ServerFetchInit.next (
NextFetchOptions.create [
NextFetchOptions.revalidate (Revalidate.seconds 900)
NextFetchOptions.tags [ "posts"; "homepage" ]
]
)
]
)The repository now also documents and demonstrates the App Router special-file flow from F#:
error.jsandglobal-error.jsviaErrorBoundaryPropstemplate.jsviaTemplatePropsdefault.jsviaDefaultProps<'T>- starter patterns for
loading.js,not-found.js,global-not-found.js,forbidden.js, andunauthorized.js
The dedicated guide is in Special files.
Inline directives work for function-level cases:
Directive.useServer()Directive.useCache()Directive.useCachePrivate()Directive.useCacheRemote()
File-level 'use client' and 'use server' directives are different. Fable emits imports first, so App Router entry files still need thin wrapper modules when the directive must appear at the top of the generated JavaScript file.
Generate them with:
node tools/nextfs-entry.mjs samples/nextfs.entries.jsonFor 'use server' wrappers, only named exports are allowed. The generator rejects default exports and export * for that case.
src/NextFscontains the bindings packagesamples/NextFs.Smokecontains compile-smoke coverage of the package surfaceexamples/nextfs-startercontains a minimal end-to-end App Router starter, includinglayout,route,proxy, instrumentation, and special-file entries generated from F#tests/nextfs-entry.test.mjscovers the wrapper generatortools/nextfs-entry.mjsgenerates directive wrapper filestools/generate-google-font-bindings.mjsregenerates theGoogleFontbinding catalog from official Next.js type definitions
The repository uses samples and examples for different jobs:
samplesare verification-oriented. They exist to compile, exercise binding shapes, and catch regressions quickly.examplesare usage-oriented. They are meant to look like real consumer projects someone could copy, inspect, or adapt.
In practice:
samples/NextFs.Smokeis a compile-smoke project for the public API surface.samples/appis generated wrapper output used as a small fixture for the wrapper tool.examples/nextfs-starteris a runnable App Router starter that demonstrates the intended project layout.
- Docs folder guide
- Examples folder guide
- Samples folder guide
- Tools folder guide
- Local tool manifest guide
- Quickstart
- Starter app walkthrough
- Data fetching and route config
- Server and client patterns
- Instrumentation
- API reference
- Directives and wrappers
- Special files
- Package design and limitations
- Starter example
dotnet tool restore
dotnet femto --validate src/NextFs/NextFs.fsproj
node --test tests/*.mjs
dotnet build NextFs.slnx -v minimal
dotnet pack src/NextFs/NextFs.fsproj -c Release -o artifacts
node tools/nextfs-entry.mjs samples/nextfs.entries.json
node tools/nextfs-entry.mjs examples/nextfs-starter/nextfs.entries.json
git diff --exit-code -- examples/nextfs-starter/app examples/nextfs-starter/proxy.js examples/nextfs-starter/instrumentation.js examples/nextfs-starter/instrumentation-client.jsThe repository includes .github/workflows/publish-nuget.yml, which publishes NextFs to nuget.org on manual dispatch or tag pushes matching v*.
Publishing uses NuGet Trusted Publishing through GitHub OIDC rather than a long-lived API key.
Package page:
Contribution workflow and commit conventions are documented in CONTRIBUTING.md.