Frank.Datastar

Frontend development has become a monstrosity, and Frank.Datastar is my response. It’s a Frank-native Datastar integration that lets the server send HTML over Server-Sent Events instead of shipping a state machine to the browser.

Frank.Datastar, shipped with Frank 7.2.0, provides:

  • A single datastar operation on a resource that manages the SSE stream for you.
  • Static Datastar.* helpers (patchElements, patchSignals, and friends) that read the HttpContext directly, with no service to register.
  • Stream-based overloads (streamPatchElements and friends) that render straight to the response for the F# view engines.
  • Full Datastar SDK ADR compliance, targeting net8.0 through net10.0.
Continue reading

Frank.Analyzers

An HTTP resource has one handler per method: one GET, one POST, and so on. That’s how Frank models a resource, and the F# type system won’t stop you from breaking the rule, but Frank.Analyzers will flag this class of bug and fail at compile time. Like the rest of Frank, it’s opt-in: a recommended, not required, package that makes an app more predictable by catching mistakes the compiler can’t.

Frank.Analyzers, shipped with Frank 7.2.0, provides:

  • A compile-time check that flags duplicate HTTP handler registrations within a resource.
  • Coverage of all nine HTTP method operations.
  • The same rule in your editor (Ionide, Visual Studio, Rider) and on the command line for CI.
  • Built on the FSharp.Analyzers.SDK, targeting net8.0, net9.0, and net10.0.
Continue reading

Frank.Auth

Authorization in Frank should read like part of the resource, not ceremony bolted onto the side. Frank.Auth, shipped with Frank 7.2.0, puts the access rules in the resource definition.

Frank.Auth provides:

  • Resource-level requirements declared in the resource CE: requireAuth, requireClaim, requireRole, and requirePolicy.
  • Host-level wiring on WebHostBuilder: useAuthentication, useAuthorization, and authorizationPolicy.
  • AND semantics, so stacked requirements all must pass.
  • A thin layer over ASP.NET Core’s own authentication and authorization, not a replacement.
Continue reading

Frank.OpenApi

OpenAPI support in Frank has been a long time coming. I opened an issue asking for it in January 2019. It’s here now, and it’s declarative: you describe what an endpoint produces and accepts, and the document falls out of that.

Frank.OpenApi shipped with Frank 7.2.0 and provides:

  • A handler computation expression that pairs a handler with its OpenAPI metadata: name, summary, tags, and the types it accepts and produces.
  • F# type schemas (records, DUs, options, collections) generated through FSharp.Data.JsonSchema.OpenApi.
  • useOpenApi to wire the services and middleware, with Scalar UI as a browser client in the box.
  • Targets for net9.0 and net10.0.
Continue reading

Frank Extensibility

Frank 7.2.0 adds a small extensibility point that lets companion libraries attach behavior to an endpoint without changing, or even knowing about, the core. This version also provides greater control for ordering middleware around routing.

Frank 7.2.0 makes two additions to the core:

  • Metadata on ResourceSpec — a list of (EndpointBuilder -> unit) convention functions applied as each endpoint is built, letting companion libraries (Auth, OpenApi, Datastar) attach endpoint metadata without changes to core Frank. Binary-breaking, but source-compatible with the empty default.
  • plugBeforeRouting — with plugBeforeRoutingWhen and plugBeforeRoutingWhenNot, for controlling middleware ordering around UseRouting().
Continue reading

FSharp.Data.JsonSchema v3.0.1

FSharp.Data.JsonSchema is three packages instead of one now, the old name still works, and a pile of bugs that had been open for years are finally fixed. Most of it shipped in 3.0.0, with a follow-up in 3.0.1, the current release.

  • FSharp.Data.JsonSchema.Core: Core JSON Schema representation types that relies on only FSharp.SystemTextJson. This library can be uses to parse a JSON Schema into F# types and F# types into a JSON Schema. However, it no longer has a specific target.
  • FSharp.Data.JsonSchema.NJsonSchema: This is equivalent to the previous version with a dependency on NJsonSchema as the target. This should be backward compatible with previous versions of the library.
  • FSharp.Data.JsonSchema.OpenApi: The new target depends on the Microsoft.OpenApi library introduced with the net9.0 framework target. This target is intended for use with generating Open API documents from ASP.NET Core applications.
Continue reading

AI DevOps Podcast

I recently had the pleasure of joining Jeffrey Palermo on the AI DevOps Podcast. You can find the episode here. We talked about several of my recent posts, including why I have delayed writing more. If you are curious, you can listen to the podcast. While you are at it, check out some of the other episodes. Jeffrey has and continues to produce excellent content.

If you want a tease, it reduces to this: after some early success using Claude to help build the Frank.OpenApi and Frank.Datastar extensions, I boldly explored some more challenging extensions and discovered the agents silently deferring, lying through type and test names, and falsely reporting success. I finally restarted the work recently after learning some techniques to avoid, identify, and otherwise resolve such issues. Some of those I’ve learned from others; others I’ve developed myself. Those will be the subject of their own, future posts.

Nothing New Under the Spec

I recently stumbled across Cameron Sjo‘s spec-compare project while working on a similar comparison of my own. The project is outstanding. Compiling, testing, and documenting this many tools and frameworks takes considerable effort, and the findings are immediately useful if you’re evaluating spec-driven approaches for your team. I’m grateful someone else did this work and shared it openly. If you haven’t explored the project yet, it’s well worth your time.

Then I read the Critical Analysis.

The practical observations about AI adherence, review burden, and especially the MDD parallel are solid work. Where it lost me was the historical framing. The analysis builds its central tension around whether SDD is “waterfall dressed in AI clothing.” This is built on several layers of popular misconception, and like a house of cards at a toddler’s birthday party, it doesn’t survive contact with much scrutiny.

Continue reading

A Flurry of Library Updates: FSharp.Data.JsonSchema and Frank

I recently carved out some time to revisit some dormant projects. The primary driver was a tic-tac-toe app inspired by Scott Wlaschin‘s Enterprise Tic-Tac-Toe series of posts and presentation and a desire to learn and test out Datastar. An upcoming post will dive deeper into that topic. In this post, I want to share a high-level overview of what’s new with FSharp.Data.JsonSchema and Frank. Subsequent posts will dive into further details.

FSharp.Data.JsonSchema 3.0.1

FSharp.Data.JsonSchema is now three packages:

  • FSharp.Data.JsonSchema.Core: Core JSON Schema representation types that relies on only FSharp.SystemTextJson. This library can be uses to parse a JSON Schema into F# types and F# types into a JSON Schema. However, it no longer has a specific target.
  • FSharp.Data.JsonSchema.NJsonSchema: This is equivalent to the previous version with a dependency on NJsonSchema as the target. This should be backward compatible with previous versions of the library.
  • FSharp.Data.JsonSchema.OpenApi: The new target depends on the Microsoft.OpenApi library introduced with the net9.0 framework target. This target is intended for use with generating Open API documents from ASP.NET Core applications.

In addition, several long overdue bug fixes and enhancements should now be resolved:

  • Recursive Types (#15): Recursive F# types no longer cause infinite loops. Self-referential DUs, records with optional self-references, and recursion through collections all generate proper $ref: "#" schemas, and a follow-up fix in 3.0.1 resolved an NJsonSchema serialization failure where Ref("#") in nullable contexts tried to look up the root reference in the definitions dictionary instead of referencing the root schema directly
  • Choice types (#22): Choice<'A,'B> through Choice<'A,…,'G> now generate clean anyOf schemas instead of the verbose internal-tag encoding
  • Anonymous records: Inline object schemas, no $ref
  • DU encoding styles: InternalTag, AdjacentTag, ExternalTag, and Untagged via a new unionEncoding parameter on Generator.Create
  • Format annotations: Proper date-time, guid, uri, duration, date, time, and byte formats for DateTime, Guid, Uri, TimeSpan, DateOnly, TimeOnly, and byte[]

Frank 7.2.0

Frank has had a long and winding history as my favorite hobby project for trying out different approaches to encoding web applications. The computation expression approach starting in (IIRC) v5.0 has stuck. The goal is still to produce an HTTP resource-style set of builders that provides a consistent means of defining HTTP resources and the ASP.NET Core WebHost in which to run them while allowing for a lot of flexibility and extensibility. As such, I’ve added some additional libraries I’ve found useful to test out the extensibility and support the new tic-tac-toe hobby project mentioned above.

Packages

  • Frank: Added Metadata field to ResourceSpec, a list of (EndpointBuilder -> unit) convention functions applied during RouteEndpointBuilder.Build(). This generic extensibility point lets companion libraries (Auth, OpenApi, etc.) attach typed endpoint metadata without requiring changes to the core Frank library. This is a binary-breaking change but source-compatible with the empty default. Also added plugBeforeRouting, plugBeforeRoutingWhen, and plugBeforeRoutingWhenNot for middleware ordering control around UseRouting().
  • Frank.Analyzers: F# Analyzer (FSharp.Analyzers.SDK) that detects duplicate HTTP handler registrations within a resource block at compile time, enforcing the constraint of a single HTTP method per resource. It works in IDEs (Ionide, VS, Rider) and CLI (dotnet fsharp-analyzers) for CI/CD.
  • Frank.Auth: Adds WebHostBuilder registration and resource-level authorization via ResourceBuilder extensions, including requireAuth, requireClaim, requireRole, requirePolicy using AND semantics for resources and useAuthentication, useAuthorization, authorizationPolicy for WebHostBuilder.
  • Frank.OpenApi: Adds long-planned, declarative OpenAPI 3.0+ document generation, including a handler computation expression for pairing handlers with metadata (name, summary, tags, produces, accepts). F# type schemas (records, DUs, options, collections) via FSharp.Data.JsonSchema.OpenApi. useOpenApi on WebHostBuilder wires services and middleware. Includes Scalar UI to provide a web-based client for viewing and testing endpoints. Targets net9.0/net10.0.
  • Frank.Datastar: Native SSE implementation similar to the StarFederation.Datastar.FSharp library. Zero-copy buffer writing via IBufferWriter, zero external NuGet dependencies, full Datastar SDK ADR compliance. Added stream-based overloads (streamPatchElements, etc.) accepting TextWriter -> Task for zero-allocation HTML rendering. No breaking API changes. Targets net8.0/net9.0/net10.0.

New Samples

  • Frank.Datastar.Basic: RESTful hypermedia patterns (click-to-edit, search, bulk ops) using Frank.Datastar with F# string templates.
  • Frank.Datastar.Hox: Same patterns as Basic using the Hox view engine. Demonstrates stream-based SSE overloads via Render.toStream.
  • Frank.Datastar.Oxpecker: Same patterns as Basic using Oxpecker.ViewEngine.
  • Frank.OpenApi.Sample: Product catalog API demonstrating the handler CE with OpenAPI metadata, mixed plain/enriched handlers, useOpenApi, and Scalar UI.

Looking Ahead

I’ll spend some time in upcoming posts exploring each of these. In the meantime, I’d love feedback on any of the updates and changes. I hope I haven’t broken anyone with changes to FSharp.Data.JsonSchema, and there is a transition package with version 3.0.0 to make it easier to switch without changing package names. I don’t have any additional plans at the moment for either of these, but please open issues if you have ideas or bug reports.

Leadership, Failure, and Erlang

I’ve been reflecting on leadership recently and recalled an idea that struck me during my time leading organizations in the Texas A&M University Memorial Student Center Student Programs Office. In organizations that turned over leadership each year, the leadership followed a predictable pattern of flipping from strong leadership to weak leadership, then from weak to strong, as if on a loop. I developed the idea that in subsequent generations, strong leaders beget weak leaders, and weak leaders beget strong leaders. Based on this observation and idea, I developed a practice of being intentionally weak in certain circumstances in order to develop leadership in those I led.

Before we continue, let me define terms as I’m using them:

  • strong – proactive, decisive, quick to correct, and minimize risk of failure
  • weak – passive, indecisive, leaves a gap that needs to be filled, risk of failure

When I’ve searched for these terms online, I typically see something that looks more like a contrast of “good” versus “bad” leaders, for varying definitions of “good” and “bad.” When I use “strong” and “weak,” I assume both exhibit “good” leadership qualities, for whatever definition you wish to use for “good.”

The practice I tried to develop while at Texas A&M was to identify and intentionally give space for others to step up and grow as leaders. I provided backup to minimize the impact of failure and follow-up with a retrospective to learn and grow. This was moderately successful, in large part because I had a lot of room to learn and grow myself. Thankfully, the advisors at Texas A&M were wonderful and provided the same kind of environment to grow as leaders.

I’ve found this approach continues to work well throughout my career, though I have forgotten to use it at times. I recently started exploring new (to me) programming languages and came across Gleam, a typed, functional programming language for the Erlang runtime. Erlang is known for its resilience and fault tolerance, yet it achieves this by means of a “Let it Crash” philosophy. This seems counterintuitive. Success through failure? In Programming Erlang, Joe Armstrong notes that the difference is in expecting failure, one can focus instead on planning how to identify and recover.

My great concern is not whether you have failed, but whether you are content with your failure. - Abraham Lincoln

There is a correlation between the “Let it Crash” philosophy and growing leadership abilities. We tend to think of success as good and failure as bad, but failure is only bad if it does not translate into a learning opportunity. Successful and unsuccessful outcomes can both be positive outcomes, but they need to be planned. Planning involves identifying opportunities for each person you want to grow in leadership, assessing risk, and providing for contingencies.

You may be wondering how this is different than coaching. I see the difference in coaching is an explicitly communicated opportunity, whereas what I propose above is not explicit. You have to make room for others to identify and then pursue the opportunity on their own. Coaching should certainly be part of the process, but it falls into the “strong” leadership category.

Leaving room for others is challenging. It means waiting on making improvements. You may get only a partial solution. However, your people will struggle to make it to the next step without opportunities. I’ve enjoyed reflecting on and rediscovering this approach. I’d love to know how others approach leadership development in their people. Let me know in the comments.