{"id":33191344,"url":"https://github.com/cmeeren/Felicity","last_synced_at":"2025-11-21T00:02:10.556Z","repository":{"id":54602106,"uuid":"241107034","full_name":"cmeeren/Felicity","owner":"cmeeren","description":"Boilerplate-free, idiomatic JSON:API for your beautiful, idiomatic F# domain model. Optimized for developer happiness.","archived":false,"fork":false,"pushed_at":"2025-10-03T07:31:03.000Z","size":2007,"stargazers_count":94,"open_issues_count":1,"forks_count":1,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-11-11T13:27:54.446Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"F#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cmeeren.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":".github/CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2020-02-17T12:52:27.000Z","updated_at":"2025-10-28T23:55:51.000Z","dependencies_parsed_at":"2023-12-20T15:57:33.082Z","dependency_job_id":null,"html_url":"https://github.com/cmeeren/Felicity","commit_stats":{"total_commits":394,"total_committers":2,"mean_commits":197.0,"dds":0.4289340101522843,"last_synced_commit":"7aeb1fac02971feb4789946de18835942d95dc61"},"previous_names":[],"tags_count":131,"template":false,"template_full_name":null,"purl":"pkg:github/cmeeren/Felicity","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cmeeren%2FFelicity","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cmeeren%2FFelicity/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cmeeren%2FFelicity/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cmeeren%2FFelicity/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cmeeren","download_url":"https://codeload.github.com/cmeeren/Felicity/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cmeeren%2FFelicity/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":285532343,"owners_count":27187706,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-11-20T02:00:05.334Z","response_time":54,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-11-16T06:00:41.329Z","updated_at":"2025-11-21T00:02:10.550Z","avatar_url":"https://github.com/cmeeren.png","language":"F#","funding_links":[],"categories":["Web Frameworks"],"sub_categories":["Creating Type Providers"],"readme":"Felicity\n==============\n\n\u003cimg src=\"https://raw.githubusercontent.com/cmeeren/Felicity/master/felicity-logo.png\" width=\"300\" align=\"right\" /\u003e\n\n**Boilerplate-free, idiomatic JSON:API for your beautiful, idiomatic F# domain model. Optimized for developer happiness.**\n\n### Elevator pitch\n\nFelicity is a framework that allows you to expose your functional F# domain logic as an API\nfollowing [the JSON:API specification](https://jsonapi.org/), with no boilerplate.\n\nCore features:\n\n* Based on ASP.NET Core and [Giraffe](https://github.com/giraffe-fsharp/Giraffe)\n* Thoroughly tested\n* Highly succinct, declarative and discoverable fluent-style syntax\n* Designed for with idiomatic, immutable F# domain logic, including DU wrappers and smart constructors\n* Automatic support for sparse fieldsets and included resources\n* Automatic routing and link generation for resources and relationships\n* Any attribute, relationship, or operation may be async or return errors\n* Loads included resources on-demand and in parallel\n* Easily pass strongly typed context data (e.g. authentication information) through Felicity to any part of your code\n* Over 100 common error cases handled automatically; you only have to care about your application-specific errors\n* If it compiles and starts, it works\n* Support for polymorphic resource collections and relationships\n* Built-in support for resource-level precondition validation using ETags and modification dates (requiring the client\n  to supply `If-Match` and `If-Unmodified-Since` to avoid “mid-air collisions”)\n* Resource-level locking and operation queueing to ensure thread safety (can also lock “parent” resources and plug into\n  external locking mechanisms)\n* Supports sideposting (a.k.a. sideloading) to create a related resource hierarchy in a single POST request (not in\n  official JSON:API spec)\n\n### Production readiness\n\nThis framework contains ~1000 end-to-end tests checking success-path and error-path functionality of all operations,\nand is used in several mission-critical production APIs at our company. I’m not claiming it’s perfect, or even bug-free,\nbut it’s well tested, and I have a vested interest in keeping this framework working properly.\n\nThe framework is still at 0.x because it's still fairly new and I'm still discovering improvements that require breaking\nchanges every now and then. However, do not take 0.x to mean that it’s a buggy mess, or that the API will radically\nchange every other week.\n\n### A note on versioning\n\nWhile at 0.x, I’ll try to increment the minor version for breaking changes and the patch version for anything else.\n\nNote that “breaking changes”, at least for now, only consider idiomatic use of the framework. For example, you should\nnever need to use type annotations for the many “builder types” used by Felicity, so I may rename and refactor those and\nnot consider it a breaking change as long as the final intended syntax stays the same.\n\nContributing\n------------\n\nContributions and ideas are welcome! Please\nsee [Contributing.md](https://github.com/cmeeren/Felicity/blob/master/.github/CONTRIBUTING.md) for details.\n\nDocumentation\n-------------\n\n[Documentation is in progress.](https://github.com/cmeeren/Felicity/blob/master/DOCUMENTATION.md) A lot is already done.\n\nI also highly recommend you check out\nthe [sample API](https://github.com/cmeeren/Felicity/tree/master/src/Felicity.SampleApi) in this repo, which is a simple\nbut complete and almost-production-ready example API implementation. Open the main solution in VS, start at the topmost\nfile in the sample API, and read through the project in compilation order. There are lots of comments along the way to\nexplain what’s going on.\n\nFeel free to open an issue if you have questions.\n\nQuick start\n-----------\n\n### Assumptions about your domain logic\n\nYour core logic must be “pure” in the sense that it must not cause observable state changes (such as mutate objects or\npersist changes to a database). In other words, field “setters” should have signatures like\n`'arg -\u003e 'entity -\u003e 'entity`, returning a new entity (typically an updated record). This is a requirement because any\nsetter may potentially return an error, in which case an error response should be returned, which means that no\nobservable state changes must have taken place while executing the setters.\n\nAny part of your domain logic may be asynchronous and/or return `Result`, and may accept an API-specific context type\nyou define (that may, for example, contain an authenticated user object). For example, the general signature for a\n“setter” is\n\n```f#\n'ctx -\u003e 'arg -\u003e 'entity -\u003e Async\u003cResult\u003c'entity, Error list\u003e\u003e\n```\n\nwhere `Error` is a Felicity-defined type representing a JSON:API error. (Your domain logic may of course return error\nDUs which you map to `Error` objects at a higher, API-specific level.) Felicity has tons of overloads for simpler\nsignatures for all operations (e.g. without context, no async, no result, etc.). The goal is to enable you to simply\nplug your existing domain functions directly into Felicity without needing to use lambdas or lifting to Result or async.\n\nHere is an example of simple domain logic that works well with Felicity:\n\n```f#\ntype PersonId = PersonId of Guid\ntype FirstName = FirstName of string\ntype LastName = LastName of string\n\ntype Person = {\n    Id: PersonId\n    FirstName: FirstName\n    LastName: LastName\n}\n\nmodule Person =\n\n    let create firstName lastName = {\n        Id = Guid.NewGuid() |\u003e PersonId\n        FirstName = firstName\n        LastName = lastName\n    }\n\n    let setFirstName firstName (person: Person) = { person with FirstName = firstName }\n\n    let setLastName lastName (person: Person) = { person with LastName = lastName }\n```\n\n### Installation\n\nInstall [Felicity](https://www.nuget.org/packages/Felicity) from NuGet.\n\n### Usage\n\nI highly recommend you check out\nthe [sample API](https://github.com/cmeeren/Felicity/tree/master/src/Felicity.SampleApi) in this repo, which is a simple\nbut complete and almost-production-ready example API implementation. Open the main solution in VS, start at the topmost\nfile in the sample API, and read through the project in compilation order. There are lots of comments along the way to\nexplain what’s going on.\n\nAs a very short introduction, I hope the steps below are useful, but bear in mind that they only skim the surface (\nhowever, while Felicity has many more features than shown below, it doesn’t really get much more complicated).\n\n#### 0. Open Felicity\n\nEverything is available under the `Felicity` namespace.\n\n```f#\nopen Felicity\n```\n\n#### 1. Define errors you need to return\n\nYou don’t actually do this *first*; just define errors as you need them. We need an error for the example in the next\nsection.\n\n```f#\n[\u003cAutoOpen\u003e]\nmodule Errors =\n\n    let unauthorized =\n        Error.create 401\n        |\u003e Error.setTitle \"Unauthorized\"\n        |\u003e Error.setDetail \"The authorization was missing or invalid for this operation\"\n```\n\n#### 2. Define your global context type and how to create it from `HttpContext`\n\nFelicity allows you to map the ASP.NET Core `HttpContext` to a value you can optionally access in all operations, called\na “context” (or “ctx” for short). This value may for example contain authentication data. You can place anything you\nwant here.\n\n```f#\ntype Principal =\n    | Anonymous\n    | Authenticated of Username\n\ntype Context = { Principal: Principal }\n\nmodule Context =\n\n    // Simulate asynchronous authentication (e.g. DB or external auth service)\n    let getCtx (ctx: HttpContext) = async {\n        if false then\n            return Error [ unauthorized ]\n        else\n            return Ok { Principal = Anonymous }\n    }\n```\n\n#### 3. Define resource modules\n\nEach module corresponds to a resource. See\nthe [sample API](https://github.com/cmeeren/Felicity/tree/master/src/Felicity.SampleApi) for helpful comments for each\ndefinition; they are removed below for brevity.\n\n```f#\nmodule Article =\n\n    let define = Define\u003cContext, Article, ArticleId\u003e()\n\n    let resId =\n        define.Id.ParsedOpt(ArticleId.toString, ArticleId.fromString, (fun a -\u003e a.Id))\n\n    let resourceDef = define.Resource(\"article\", resId).CollectionName(\"articles\")\n\n    let title =\n        define.Attribute\n            .Parsed(ArticleTitle.toString, ArticleTitle.fromString)\n            .Get(fun a -\u003e a.Title)\n            .Set(Article.setTitle)\n\n    let body =\n        define.Attribute\n            .Parsed(ArticleBody.toString, ArticleBody.fromString)\n            .Get(fun a -\u003e a.Body)\n            .Set(Article.setBody)\n\n    let articleType =\n        define.Attribute\n            .Enum(ArticleType.toString, ArticleType.fromStringMap)\n            .Get(fun a -\u003e a.Type)\n            .Set(Article.setType)\n\n    let createdAt = define.Attribute.Simple().Get(fun a -\u003e a.CreatedAt)\n\n    let updatedAt = define.Attribute.Nullable.Simple().Get(fun a -\u003e a.UpdatedAt)\n\n    let author =\n        define.Relationship\n            .ToOne(Person.resourceDef)\n            .GetAsync(Db.Person.authorForArticle)\n            .Set(Article.setAuthor)\n\n    let comments =\n        define.Relationship\n            .ToMany(Comment.resourceDef)\n            .GetAsync(Db.Comment.allForArticle)\n\n    let getCollection =\n        define.Operation.GetCollection(fun ctx parser -\u003e\n            parser\n                .For(ArticleSearchArgs.empty)\n                .Add(ArticleSearchArgs.setTitle, Filter.Field(title))\n                .Add(ArticleSearchArgs.setTypes, Filter.Field(articleType).List)\n                .Add(ArticleSearchArgs.setSort, Sort.Enum(ArticleSort.fromStringMap))\n                .Add(ArticleSearchArgs.setOffset, Page.Offset)\n                .Add(ArticleSearchArgs.setLimit, Page.Limit.Max(20))\n                .BindAsync(Db.Article.search))\n\n    let post =\n        define.Operation\n            .Post(fun ctx parser -\u003e parser.For(Article.create, author, title, body))\n            .AfterCreateAsync(Db.Article.save)\n\n    let lookup = define.Operation.LookupAsync(Db.Article.byId)\n\n    let get = define.Operation.GetResource()\n\n    let patch =\n        define.Operation\n            .Patch()\n            .AfterUpdateAsync(fun a -\u003e async {\n                let a = a |\u003e Article.setUpdated (Some DateTimeOffset.Now)\n                do! Db.Article.save a\n                return a\n            })\n\n    let delete = define.Operation.DeleteAsync(Db.Article.delete)\n```\n\n#### 5. Register a JSON:API handler for the context type in `ConfigureServices` and register/add the routes\n\n```f#\ntype Startup() =\n\n    member _.ConfigureServices(services: IServiceCollection) : unit =\n        services\n            .AddGiraffe()\n            .AddRouting()\n            .AddJsonApi()\n            .GetCtxAsyncRes(Context.getCtx)\n            .Add()\n            .AddOtherServices( (* ... *) )\n\n    member _.Configure(app: IApplicationBuilder, env: IWebHostEnvironment) : unit =\n        app\n            .UseGiraffeErrorHandler(fun ex _ -\u003e\n                Log.Error(ex, \"Unhandled exception while executing request\")\n                returnUnknownError)\n            .UseRouting()\n            .UseJsonApiEndpoints\u003cContext\u003e()\n        |\u003e ignore\n```\n\n#### 6. Enable server garbage collection\n\nFor performance reasons, you should enable server GC if you are running the API on multiple cores. (Cursory testing on\nmy dev machine indicates a ~30% speedup in Felicity code just from this change alone, which is noticeable for very large\nresponses.) Refer to your target environment documentation (Azure, IIS, etc.) for details on whether this is enabled by\ndefault, and how to enable it if not. You can find some general\ninformation [here](https://docs.microsoft.com/en-us/dotnet/core/run-time-config/garbage-collector).\n\n#### 7. Profit!\n\nThat’s it! You now have a wonderfully compliant JSON:API exposing your wonderfully functional F# domain logic.\n\nRelease notes\n-------------\n\n[RELEASE_NOTES.md](https://github.com/cmeeren/Felicity/blob/master/RELEASE_NOTES.md)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcmeeren%2FFelicity","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcmeeren%2FFelicity","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcmeeren%2FFelicity/lists"}