{"id":17921867,"url":"https://github.com/zth/res-x","last_synced_at":"2025-10-09T20:21:43.926Z","repository":{"id":205859129,"uuid":"715264674","full_name":"zth/res-x","owner":"zth","description":"A ReScript framework for building server-driven web sites and applications. Use familiar tech like JSX and the component model from React, combined with simple server driven client side technologies like HTMX. Built on Bun and Vite.","archived":false,"fork":false,"pushed_at":"2025-09-18T11:32:12.000Z","size":949,"stargazers_count":69,"open_issues_count":10,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-09-28T15:59:23.195Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"ReScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zth.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2023-11-06T19:46:37.000Z","updated_at":"2025-09-22T07:06:52.000Z","dependencies_parsed_at":"2023-12-11T21:24:56.181Z","dependency_job_id":"5c29e6c2-8dcf-4bc7-86d7-45e855963795","html_url":"https://github.com/zth/res-x","commit_stats":null,"previous_names":["zth/res-x"],"tags_count":20,"template":false,"template_full_name":null,"purl":"pkg:github/zth/res-x","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zth%2Fres-x","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zth%2Fres-x/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zth%2Fres-x/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zth%2Fres-x/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zth","download_url":"https://codeload.github.com/zth/res-x/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zth%2Fres-x/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279002015,"owners_count":26083258,"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-10-09T02:00:07.460Z","response_time":59,"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":"2024-10-28T20:36:31.656Z","updated_at":"2025-10-09T20:21:43.917Z","avatar_url":"https://github.com/zth.png","language":"ReScript","funding_links":[],"categories":["ReScript"],"sub_categories":[],"readme":"# ResX\n\nA ReScript framework for building server-driven web sites and applications. Use familiar tech like JSX and the component model from React, combined with simple server driven client side technologies like HTMX. Built on Bun and Vite.\n\nResX is suitable for building everything from blogs to complex web applications.\n\n## Philosophy\n\nResX focuses on the web platform, and aims to see how far we can get building web sites and applications before reaching for a full blown client side framework is necessary.\n\nResX has an \"open hood\". That means that it's trying to stay close to the metal, and have fairly few abstractions. It encourages you to understand how a web server and the web platform works. This will lead to you building better and more robust things as you're encouraged to understand the platform itself.\n\n## Demo\n\n_The demo is currently a WIP._\nThe `demo/` will contain a comprehensive example of using ResX.\n\n## Getting started\n\nFirst, make sure you have [`Bun`](https://bun.sh) installed and setup. Then, install `rescript-x` and the dependencies needed:\n\n```bash\nnpm i rescript-x vite @rescript/core rescript-bun\n```\n\nNote that ResX requires these versions:\n\n- `rescript@\u003e=11.1.0-rc.2`\n- `@rescript/core@\u003e=1.0.0`\n- `rescript-bun@\u003e=0.4.1`\n\nConfigure our `rescript.json`:\n\n```json\n{\n  \"jsx\": {\n    \"module\": \"Hjsx\"\n  },\n  \"bs-dependencies\": [\"@rescript/core\", \"rescript-x\", \"rescript-bun\"],\n  \"bsc-flags\": [\n    \"-open RescriptCore\",\n    \"-open RescriptBun\",\n    \"-open RescriptBun.Globals\",\n    \"-open ResX.Globals\"\n  ]\n}\n```\n\nGo ahead and install the dependencies for Tailwind as well if you want to use it:\n\n```bash\nnpm i autoprefixer postcss tailwindcss\n```\n\nLet's set everything up. Start by setting up `vite.config.js`:\n\n```javascript\nimport { defineConfig } from \"vite\";\nimport { resXVitePlugin } from \"rescript-x\";\n\nexport default defineConfig({\n  plugins: [resXVitePlugin()],\n  server: {\n    port: 9000,\n  },\n});\n```\n\nMake sure you have both folders for static assets set up: `assets` and `public` in the root, next to `vite.config.js`. More on static assets later.\n\nIf you're using Tailwind, add `tailwind.config.js` and `postcss.config.js` as well:\n\n```javascript\n// postcss.config.js\nmodule.exports = {\n  plugins: [require(\"tailwindcss\"), require(\"autoprefixer\")],\n};\n```\n\n```javascript\n// tailwind.config.js\n/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  content: [\"./src/**/*.res\"],\n  theme: {\n    extend: {},\n  },\n  plugins: [],\n};\n```\n\nThere! If you want, you can also set up a bunch of scripts in `package.json` that'll make life easier:\n\n```json\n{\n  \"scripts\": {\n    \"start\": \"NODE_ENV=production bun run src/App.js\",\n    \"build\": \"NODE_ENV=production \u0026\u0026 bun run build:vite \u0026\u0026 bun run build:res\",\n    \"build:vite\": \"vite build\",\n    \"build:res\": \"rescript\",\n    \"clean:res\": \"rescript clean\",\n    \"dev:res\": \"rescript build -w\",\n    \"dev:server\": \"bun --watch run src/App.js\",\n    \"dev:vite\": \"vite\",\n    \"dev\": \"concurrently 'bun:dev:*'\"\n  }\n}\n```\n\n\u003e Note: These scripts use `concurrently`. Install via `npm i concurrently`.\n\nNow, let's create your `Handler` instance. You'll use this throughout your app as a sort of context:\n\n```rescript\n// Handler.res\n\n// This context will be passed throughout your application. Use it for any per-request needs, like dataloaders, the id of the currently logged in user, etc.\ntype context = {userId: option\u003cstring\u003e}\n\n// `requestToContext` should produce your context above from the pending `request`. It'll be called fresh for each request.\nlet handler = ResX.Handlers.make(~requestToContext=async _request =\u003e {\n  userId: None,\n})\n\n// This isn't required but is a shorthand to pull out the context a bit more conveniently from your handler.\nlet useContext = () =\u003e ResX.Handlers.useContext(handler)\n```\n\nNext, let's set up our webserver via Bun:\n\n```rescript\n// App.res\nlet port = 4444\n\nlet server = Bun.serve({\n  port,\n  development: ResX.BunUtils.isDev,\n  fetch: async (request, server) =\u003e {\n    open Bun\n\n    // Serve static files first\n    switch await ResX.BunUtils.serveStaticFile(request) {\n    | Some(staticResponse) =\u003e staticResponse\n    | None =\u003e\n      // Handle the request using the ResX handler if this wasn't a static file request.\n      // Note: By default, all HTMX handler routes are prefixed with \"_api\", and all form action routes are prefixed with \"_form\".\n      await Handler.handler-\u003eResX.Handlers.handleRequest({\n        request,\n        setupHeaders: () =\u003e {\n          // You can do any basic headers setup here that you want. These can be overwritten easily by your main application regardless of what you set here.\n          Headers.makeWithInit(FromArray([(\"Content-Type\", \"text/html\")]))\n        },\n        render: async ({path, requestController, headers}) =\u003e {\n          // This handles the actual request.\n          switch path {\n          | list{\"sitemap.xml\"} =\u003e \u003cSiteMap /\u003e\n          | appRoutes =\u003e\n            requestController-\u003eResX.RequestController.appendTitleSegment(\"Test App\")\n            \u003cHtml\u003e\n              \u003cdiv\u003e\n                {switch appRoutes {\n                | list{} =\u003e\n                  \u003cdiv\u003e {Hjsx.string(\"Start page!\")} \u003c/div\u003e\n                | list{\"moved\"} =\u003e\n                  requestController-\u003eResX.RequestController.redirect(\"/start\", ~status=302)\n                | _ =\u003e\n                  requestController-\u003eResX.RequestController.setStatus(404)\n                  \u003cdiv\u003e{Hjsx.string(\"404\")}\u003c/div\u003e\n                }}\n              \u003c/div\u003e\n            \u003c/Html\u003e\n          }\n        },\n      })\n    }\n  },\n})\n\nlet portString = server-\u003eBun.Server.port-\u003eInt.toString\n\nConsole.log(`Listening! on localhost:${portString}`)\n\n// Run the dev server, responsible for hot module reloading etc, when in dev mode.\nif ResX.BunUtils.isDev {\n  ResX.BunUtils.runDevServer(~port)\n}\n```\n\nNote that there's plenty of more things you can configure here, but for the sake of keeping it simple we'll just go with the basics.\n\nYou can now start up the dev environment: `bun run dev`. Open up `localhost:9000` and you should see your \"Start page!\" string.\n\nThere's a ton more to ResX of course, but this should get you started.\n\n### Routing\n\nAs you noticed from the example above, there's no explicit router in ResX itself. In the future, we might ship a dedicated type safe router in the style of [rescript-relay-router](https://github.com/zth/rescript-relay-router). But for now, we'll use pattern matching!\n\nYou route by just pattern matching on `path`:\n\n```rescript\nswitch path {\n| list{} =\u003e\n  // Path: /\n  \u003cdiv\u003e {Hjsx.string(\"Start page!\")} \u003c/div\u003e\n| list{\"moved\"} =\u003e\n  // Path: /moved\n  requestController-\u003eResX.RequestController.redirect(\"/start\", ~status=302)\n| _ =\u003e\n  // Any other path\n  requestController-\u003eResX.RequestController.setStatus(404)\n  \u003cdiv\u003e{Hjsx.string(\"404\")}\u003c/div\u003e\n}\n```\n\n## Static assets\n\nResX comes with full static asset (fonts, images, etc) handling via Vite, that you can use if you want. In order to actually serve the static assets, make sure you use `ResX.BunUtils.serveStaticFile` before trying to handle your request in another way:\n\n```rescript\nfetch: async (request, server) =\u003e {\n    open Bun\n\n    switch await ResX.BunUtils.serveStaticFile(request) {\n    | Some(staticResponse) =\u003e staticResponse\n    | None =\u003e\n      await Handler.handler-\u003eResX.Handlers.handleRequest({\n        ...\n```\n\n`ResX.BunUtils.serveStaticFile` check if the request is for a static file, and if it is return a response serving that static file via `Bun`. If it's not a static file request, you continue as usual with serving the response.\n\nAs for the assets themselves, there are two ways of handling them in ResX:\n\n### `public` for assets that don't need transformation\n\nPutting assets in the `public` directory. Any assets you put in the top level `public` directory next to `vite.config.js` will be copied as-is to your production environment. It's then available to you via the top level:\n\n```\n// public/robots.txt exists\nGET /robots.txt\n```\n\n### `assets` for assets that do need transformation\n\nIf you have assets you'd like transformed by Vite before using, put them in the top level `assets` folder. This could be CSS, images, additional JavaScript, and so on. Anything you might want Vite to transform.\n\nHere's an example of how you wire up Tailwind:\n\n```css\n/* assets/styles.css */\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n```\n\nThen, include it in your ReScript:\n\n```rescript\n\u003chead\u003e\n  \u003clink type_=\"text/css\" rel=\"stylesheet\" href={ResXAssets.assets.styles_css} /\u003e\n\u003c/head\u003e\n```\n\nThere! It's now available to you, and Vite will both transform and hot module reload the asset if it's possible.\n\n#### Referring to transformed `assets`\n\nNotice how we're not using a `\"/assets/styles.css\"` string to refer to `styles.css`, but rather `ResXAssets.assets.styles_css`? This is because ResX comes with a \"type safe\" asset layer - anything you put in `assets/` will be available via `ResXAssets.assets`.\n\n**Always use this to refer to assets**. One for the type safety of course, but also because this is how Vite keeps track of all asset files, so you get the transformed asset in production, and so on.\n\n\u003e Bonus: Since the asset map is a regular ReScript record, you'll automatically get dead code analysis via the ReScript code analyzer. Dead code analysis for your assets! Makes it really easy to keep your assets folder clean.\n\n## ResX client side tools\n\nResX wants you to think \"server side rendering\" as much as possible. In order to allow you to take this as far as possible, ResX ships with 2 client side libraries that's intended to help you solve as many cases where client side JavaScript is needed as possible.\n\n### HTMX\n\nResX comes pre-baked with full HTMX support.\n\nMake sure you include the HTMX script:\n\n```rescript\n\u003cscript src=\"https://unpkg.com/htmx.org@1.9.5\" /\u003e\n```\n\n#### hx-get, hx-post and friends\n\nResX has first class support for using `hx-get`, `hx-post` and friends from HTMX. There are two ways to use each `hx` attribute:\n\n1. Via the builtin ResX HTML attribute `hx\u003cmethod\u003e`. So, for example `hxGet`. This is the recommended way. Details below on how to use this.\n2. Putting a raw string on the `hx\u003cmethod\u003e` attribute. This is useful when you want to use HTMX with a route URL that you don't want to go through the regular ResX handling. Every `hx\u003cmethod\u003e` comes with an equivalent `rawHx\u003cmethod\u003e` prop, that takes a plain string. So you could do this: `rawHxGet={\"/some/path\"}`.\n\nIn the vast majority of cases you'll likely use number 1. In order to use 1., you create a `htmxHandler`, and then attach actions to that handler. You then pass those actions to `hxGet`, `hxPost` etc. Here's a simple example.\n\nFirst, set up your `htmxHandler`. This maker takes a `requestToContext` function, that's responsible for translating a request into a (per-request) context. This is where you put the current user ID, dataloaders, or whatever else you want to have available through the lifetime of your request.\n\n```rescript\n// Handler.res\ntype context = {userId: option\u003cstring\u003e}\n\nlet handler = ResX.Handlers.make(~requestToContext=async request =\u003e {\n  // Pull out the current user ID from the request, if it exists\n  userId: Some(\"some-user-id\"),\n})\n\n// Short hand for retrieving the context\nlet useContext = () =\u003e handler-\u003eResX.Handlers.useContext\n```\n\nNow, we can attach and use actions via this handler:\n\n```rescript\n// User.res\nlet onForm = Handler.handler-\u003eResX.Handlers.hxPost(\"/user-single\", ~securityPolicy=ResX.SecurityPolicy.allow, ~handler=async ({request}) =\u003e {\n  let formData = await request-\u003eRequest.formData\n  try {\n    let name = formData-\u003eResX.FormDataHelpers.expectString(\"name\")\n    \u003cdiv\u003e{Hjsx.string(`Hi ${name}!`)}\u003c/div\u003e\n  } catch {\n  | Exn.Error(err) =\u003e\n    Console.error(err)\n    \u003cdiv\u003e {Hjsx.string(\"Failed...\")} \u003c/div\u003e\n  }\n})\n\n@jsx.component\nlet make = () =\u003e {\n  \u003cform\n    hxPost={onForm}\n    hxSwap={ResX.Htmx.Swap.make(InnerHTML)}\n    hxTarget={ResX.Htmx.Target.make(CssSelector(\"#user-single\"))}\u003e\n    \u003cinput type_=\"text\" name=\"name\" /\u003e\n    \u003cdiv id=\"user-single\"\u003e\n      {Hjsx.string(\"Hello...\")}\n    \u003c/div\u003e\n    \u003cbutton\u003e{Hjsx.string(\"Submit\")}\u003c/button\u003e\n  \u003c/form\u003e\n}\n```\n\nThis is all wired up automatically via `ResX.Handlers.handleRequest`. Also notice that as all of this is server side, you don't need to worry about accidentally leaking things to the client.\n\n##### Handling cyclic dependencies\n\nSometimes you end up in a situation where you want to refer to the `hxGet` (or any other `hx` handler) you're implementing inside of the implementation itself. For example, a component that can \"refresh\" itself. This can't be done with the regular `ResX.Handlers.get` etc because that'd create a situation of cyclic dependencies where the definition of the handler refers to itself. In order to handle these specific scenarios, you can leverage `ResX.Handlers.makeGet` + `ResX.Handlers.implementGet` to first get a `hxGet` identifier you can attach to your DOM nodes, and _then_ implement it in a place where you won't get cyclic dependencies.\n\nLet's look at the example above and adjust it to work that way instead:\n\n```rescript\n// User.res\nlet onForm = Handler.handler-\u003eResX.Handlers.makeHxPostIdentifier(\"/user-single\")\n\nHandler.handler-\u003eResX.Handlers.implementHxPostIdentifier(onForm, ~securityPolicy=ResX.SecurityPolicy.allow, ~handler=async ({request}) =\u003e {\n  let formData = await request-\u003eRequest.formData\n  try {\n    let name = formData-\u003eResX.FormDataHelpers.expectString(\"name\")\n    \u003cdiv\u003e{Hjsx.string(`Hi ${name}!`)}\u003c/div\u003e\n  } catch {\n  | Exn.Error(err) =\u003e\n    Console.error(err)\n    \u003cdiv\u003e {Hjsx.string(\"Failed...\")} \u003c/div\u003e\n  }\n})\n\n@jsx.component\nlet make = () =\u003e {\n  \u003cform\n    hxPost={onForm}\n    hxSwap={ResX.Htmx.Swap.make(InnerHTML)}\n    hxTarget={ResX.Htmx.Target.make(CssSelector(\"#user-single\"))}\u003e\n    \u003cinput type_=\"text\" name=\"name\" /\u003e\n    \u003cdiv id=\"user-single\"\u003e\n      {Hjsx.string(\"Hello...\")}\n    \u003c/div\u003e\n    \u003cbutton\u003e{Hjsx.string(\"Submit\")}\u003c/button\u003e\n  \u003c/form\u003e\n}\n```\n\nNotice how producing the `hxPost` identitifer is now separate from implementing it. This means you can put the implementation in a place where it won't suffer from circular dependencies.\n\n#### Other hx-attributes are handled type safely\n\n\u003e Note: All `hx`-attributes have equivalent `raw` versions, so you can always opt out of the type safe handling if it doesn't suite your needs.\n\nAll `hx`-attributes have type safe maker-style APIs. Let's look at the example above again:\n\n```rescript\n@jsx.component\nlet make = () =\u003e {\n  \u003cform\n    hxPost={onForm}\n    hxSwap={ResX.Htmx.Swap.make(InnerHTML)}\n    hxTarget={ResX.Htmx.Target.make(CssSelector(\"#user-single\"))}\u003e\n    \u003cinput type_=\"text\" name=\"name\" /\u003e\n    \u003cdiv id=\"user-single\"\u003e\n      {Hjsx.string(\"Hello...\")}\n    \u003c/div\u003e\n    \u003cbutton\u003e{Hjsx.string(\"Submit\")}\u003c/button\u003e\n  \u003c/form\u003e\n}\n```\n\nNotice how `hxSwap` and `hxTarget` are passed things from `Htmx.Something.make`? This is the way you interface with the typed `hx` attributes.\n\n### Security policies\n\nAll HTMX handlers and form actions require a `securityPolicy` parameter. This allows you to control access to your endpoints by evaluating each request before the handler is executed, and forces you to consider security for each endpoint you expose.\n\nA security policy is a function that takes the request and context, and returns either `Allow` or `Block` with optional error details:\n\n```rescript\n// Allow all requests\n~securityPolicy=ResX.SecurityPolicy.allow\n\n// Custom security policy\n~securityPolicy=async ({request, context}) =\u003e {\n  switch context.userId {\n  | Some(_userId) =\u003e ResX.SecurityPolicy.Allow\n  | None =\u003e ResX.SecurityPolicy.Block({\n      code: Some(401),\n      message: Some(\"Authentication required\"),\n    })\n  }\n}\n```\n\nWhen a request is blocked by a security policy, a response is returned with (optionally) the status code and message provided by the security policy function.\n\n### Regular form actions\n\nSometimes you don't need a full blown HTMX handler for handling a form action. Maybe all you want to do is redirect, or something else where you want full control over what response you return.\n\nThis is easy to do in `ResX` using a `formAction`. It's similar to a HTMX handler. Let's look at how to implement a form action that redirects as a form is submitted:\n\n```rescript\n// User.res\nlet onSubmit = Handler.handler-\u003eResX.Handlers.formAction(\"/some-url\", ~securityPolicy=ResX.SecurityPolicy.allow, ~handler=async ({request, context}) =\u003e {\n  Response.makeRedirect(\"/some-other-page\")\n})\n\n@jsx.component\nlet make = () =\u003e {\n  \u003cform action={onSubmit}\u003e\n    \u003cbutton\u003e{Hjsx.string(\"Submit and get redirected!\")}\u003c/button\u003e\n  \u003c/form\u003e\n}\n```\n\nForm actions have access to your `context` object, as well as the full `request` object. They're expected to return a `Response.t`, which you're in charge of building yourself.\n\nYou control whether you want the form method to be `POST` or `GET` via the `method` attribute on `\u003cform\u003e`, just like you normally do.\n\n### Getting endpoint URLs (Advanced)\n\n\u003e Note: This is an advanced feature for exceptional use cases. In most situations, you should pass HTMX handlers and form actions directly to their respective HTML attributes instead of extracting their URLs.\n\nIn rare cases where you need programmatic access to the actual URL string for your handlers, ResX provides helper functions:\n\n```rescript\n// For HTMX handlers\nlet getHandler = Handler.handler-\u003eResX.Handlers.hxGet(\"/api/users\", ~securityPolicy=ResX.SecurityPolicy.allow, ~handler=async _ =\u003e {\n  // handler implementation\n})\n\n// Extract the URL (advanced use only)\nlet endpointUrl = getHandler-\u003eResX.Handlers.hxGetToEndpointURL\n// endpointUrl contains the actual endpoint URL\n\n// Similar functions exist for all HTTP methods:\n// hxPostToEndpointURL, hxPutToEndpointURL, hxDeleteToEndpointURL, hxPatchToEndpointURL\n\n// For form actions\nlet submitAction = Handler.handler-\u003eResX.Handlers.formAction(\"/submit-form\", ~securityPolicy=ResX.SecurityPolicy.allow, ~handler=async _ =\u003e {\n  // handler implementation\n})\n\n// Extract the URL (advanced use only)\nlet formUrl = submitAction-\u003eResX.Handlers.FormAction.toEndpointURL\n```\n\nThese functions should only be used in exceptional cases where you need to:\n\n- Build custom JavaScript that needs to know the endpoint URLs\n- Create dynamic redirects or navigation logic that can't use the handlers directly\n- Generate API documentation or debugging tools\n\n**In the vast majority of cases, you should use the handlers directly with HTML attributes instead of extracting their URLs.**\n\n### ResX Client\n\nResX also ships with a tiny client side library that will help you do basic client side tasks fully declaratively. It's quite basic at the moment, but will be extended (tastefully) as we discover more places where it can help you avoid having to use a full blown client side framework to accomplish fairly basic tasks.\n\nTo use ResX client, make sure you include its script:\n\n```rescript\n\u003cscript src={ResXAssets.assets.resXClient_js} async=true /\u003e\n```\n\n#### Handling CSS classes on events\n\nSometimes all you need to do is add, remove or toggle a CSS class in response to something like a click somewhere. Here's how you do that with ResX:\n\n```rescript\n\u003cbutton\n  id=\"test\"\n  resXOnClick={ResX.Client.Actions.make([\n    ToggleClass({className: \"text-xl\", target: This}),\n  ])}\u003e\n  {Hjsx.string(\"Submit form\")}\n\u003c/button\u003e\n```\n\nNotice `resXOnClick`. This will trigger on any click of the button, and toggle the CSS class `text-xl` on the `button` element itself.\n\nHave a look in the `ResX.Client` module for an exhaustive list of all actions that are available and how to use them.\n\n#### Setting custom validity messages for HTML5 form validation\n\nContrary to popular belief, the [built in HTML5 form validation](https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation#using_built-in_form_validation) is actually pretty good, and will get you really far before you need to reach for client side JavaScript to validate. But, it has one glaring omission that makes it harder - you can't set custom validation messages without a fairly involved process, depending on client side JavaScript. ResX aims to fix this via `resXValidityMessage`:\n\n```rescript\n\u003cinput\n  type_=\"text\"\n  name=\"lastName\"\n  required=true\n  resXValidityMessage={ResX.Client.ValidityMessage.make({\n    valueMissing: \"Yo, you need to fill this in!\",\n  })}\n/\u003e\n```\n\nThis will turn the validity message for when the value is missing (since it's marked a `required`) into the supplied message, rather than the generic message printed by the browser.\n\n`resXValidityMessage` supports changing all available validity messages. Refer to the `ResX.Client.ValidityMessage` module for an exhaustive list.\n\n## Building UI with ResX\n\nIf you're familiar with React, JSX and the component model, building UI with ResX is very straight forward. It's essentially like using React as a templating engine, with a sprinkle of React Server Components flavor.\n\nIn ResX, you'll interface with 2 modules mainly when working with JSX:\n\n1. `Hjsx` - this holds functions like `string`, `int` etc for converting primitives to JSX, and a bunch of things that are needed for the JSX transform.\n2. `H` - this holds the `Context` module, as well as functions for turning JSX elements into strings.\n\nThe bulk of your code is going to be (reusable) components. You define one just like you do in React, with the difference that `React.string`, `React.int` etc are called `Hjsx.string` and `Hjsx.int`, and `@react.component` is called `@jsx.component` instead:\n\n```rescript\n// Greet.res\n@jsx.component\nlet make = (~name) =\u003e {\n  \u003cdiv\u003e{Hjsx.string(\"Hello \" ++ name)}\u003c/div\u003e\n}\n\n// SomeFile.res\n@jsx.component\nlet make = (~userName) =\u003e {\n  \u003cdiv\u003e\n    \u003cGreet name=userName /\u003e\n  \u003c/div\u003e\n}\n```\n\n### Rendering unescaped content\n\nBy default, all content in ResX is properly HTML-escaped for security. However, there are legitimate cases where you need to output raw content (like content from a CMS, CSV files or similar \"non HTML content\", markdown processors, or trusted HTML strings). For these cases, ResX provides `Hjsx.dangerouslyOutputUnescapedContent`:\n\n```rescript\n@jsx.component\nlet make = (~trustedHtmlContent) =\u003e {\n  \u003cdiv\u003e\n    {Hjsx.string(\"This content is escaped: \u003cscript\u003ealert('xss')\u003c/script\u003e\")}\n    {Hjsx.dangerouslyOutputUnescapedContent(trustedHtmlContent)}\n  \u003c/div\u003e\n}\n```\n\n**CRITICAL SECURITY WARNING**: `dangerouslyOutputUnescapedContent` completely bypasses HTML escaping. Never use this function with user-provided content or any untrusted data, as it can create XSS vulnerabilities. Only use this with content you trust completely, such as:\n\n- Static HTML strings in your code\n- Content from trusted CMS systems that handle their own sanitization\n- Pre-sanitized content from trusted markdown processors\n- Generated HTML from your own trusted systems\n- **Raw non-HTML content** like CSV data, or other structured data formats (see the [CSV export](#csv-export-example) example in the Doc header section)\n\nWhen outputting non-HTML content types (CSV, XML, etc.), you'll typically need to:\n\n1. Set the appropriate `Content-Type` header\n2. Remove or customize the doc header using `setDocHeader`\n3. Use `dangerouslyOutputUnescapedContent` to output the raw content without HTML escaping\n\nWhen in doubt, use `Hjsx.string` instead, which safely escapes all content.\n\n### Async components\n\nComponents can be defined using `async`/`await`. This enables you to do data fetching directly in them:\n\n```rescript\n// User.res\n@jsx.component\nlet make = async (~id) =\u003e {\n  let user = await getUser(id)\n\n  \u003cdiv\u003e{Hjsx.string(\"Hello \" ++ user.name)}\u003c/div\u003e\n}\n```\n\n\u003e WARNING! As with all async things you need to be careful to not create waterfalls, or performance will suffer. Handling that is out of scope for this readme, but following this tip will get you far - _initiate data fetching_ as far up the tree as possible. Awaiting the data is fine to do in leaf components, but it's good for perf to initiate data fetching as high up as possible, and then pass the promise of that data down the tree.\n\n### Context\n\nJust like in React, you can use context to pass data down your tree without having to prop drill it:\n\n```rescript\n// CurrentUserContext.res\nlet context = H.Context.createContext(None)\n\nlet use = () =\u003e H.Context.useContext(context)\n\nmodule Provider = {\n  let make = H.Context.provider(context)\n}\n\n@jsx.component\nlet make = (~children, ~currentUserId: option\u003cstring\u003e) =\u003e {\n  \u003cProvider value={currentUserId}\u003e {children} \u003c/Provider\u003e\n}\n\n// App.res\nlet currentUserId = request-\u003eUserUtils.getCurrentUserId\n\u003cCurrentUserContext currentUserId\u003e\n  \u003cdiv\u003e ... \u003c/div\u003e\n\u003c/CurrentUserContext\u003e\n\n// LoggedInUser.res\n// This is rendered somewhere far down in the tree\n@jsx.component\nlet make = () =\u003e {\n  switch CurrentUserId.use() {\n  | None =\u003e \u003cdiv\u003e{Hjsx.string(\"Not logged in\")}\u003c/div\u003e\n  | Some(currentUserId) =\u003e \u003cdiv\u003e{Hjsx.string(\"Logged in as: \" ++ currentUserId)}\u003c/div\u003e\n  }\n}\n```\n\n### Error boundaries\n\nJust like in React, you can protect parts of your UI from errors during render using an error boundary, using the `\u003cResX.ErrorBoundary /\u003e` component. You need to pass it a `renderError` function, and this function will be called whenever there's an error:\n\n```rescript\n\u003cResX.ErrorBoundary renderError={err =\u003e {\n  Console.error(err)\n  \u003cdiv\u003e{Hjsx.string(\"Oops, this blew up!\")}\u003c/div\u003e\n}}\u003e\n  \u003cdiv\u003e\n    \u003cComponentThatWillBlowUp /\u003e\n  \u003c/div\u003e\n\u003c/ResX.ErrorBoundary\u003e\n```\n\nYou can use as many error boundaries as you want. You're recommended to wrap your entire app with an error boundary as well.\n\n## Request conveniences\n\nResX ships with a number of conveniences for handling common things when building responses for requests.\n\n### `onBeforeBuildResponse` hook for manipulating the context before the response is built\n\n`onBeforeBuildResponse` lets you manipulate your request specific context before ResX starts generating HTML. Let's look at an example of adding a script tag to the head if a certain criteria has been met:\n\n```rescript\nawait Handler.handler-\u003eResX.Handlers.handleRequest({\n  request,\n  onBeforeBuildResponse: ({context, request}) =\u003e {\n    // Imagine `shouldLoadHtmx` can be set to true by the code that has executed for this particular route. A component could for example mark itself as needing HTMX.\n    if context.shouldLoadHtmx {\n      response-\u003eResX.RequestController.appendToHead(\u003cscript src=\"https://unpkg.com/htmx.org@1.9.5\" async=true /\u003e)\n    }\n  },\n  render: async ({path, requestController, headers}) =\u003e {\n    // This handles the actual request.\n    ...\n```\n\n### `onBeforeSendResponse` hook for manipulating the final response before sending it\n\n`onBeforeSendResponse` lets you manipulate the response you're producing one last time before sending it to the client. Let's look at an example of overriding any cache header set when the user is logged in:\n\n```rescript\nawait Handler.handler-\u003eResX.Handlers.handleRequest({\n  request,\n  onBeforeSendResponse: ({context, response, request}) =\u003e {\n    // Change (or replace) the final response here.\n    if context.isLoggedIn {\n      response-\u003eResponse.headers-\u003eHeaders.set(\"Cache-Control\", \"no-store, no-cache\"))\n    }\n\n    response\n  },\n  render: async ({path, requestController, headers}) =\u003e {\n    // This handles the actual request.\n    ...\n```\n\nThis way, you can conveniently make sure that no logged in pages are cached, and so on.\n\n### `\u003ctitle\u003e` integration\n\nIt's nice to be able to set the `\u003ctitle\u003e` incrementally as you render your app. But, `\u003ctitle\u003e` belongs in `\u003chead\u003e` and when you render `\u003chead\u003e` you probably don't have everything you need to produce the title you want.\n\nTherefore, ResX ships with a helper for handling the title using `ResX.RequestController`. This helper lets you either _append_ items to the title, or _set the full title_. You can then easily build your title element as you render your app, without having to know the full title as you render `\u003chead\u003e`:\n\n```rescript\n// App.res\nlet context = ResX.Handlers.useContext(HtmxHandler.handler)\ncontext.requestController-\u003eResX.RequestController.prependTitleSegment(\"My App\")\n\n// Users.res\n// Title is now \"Users | MyApp\"\ncontext.requestController-\u003eResX.RequestController.prependTitleSegment(\"Users\")\n\n// SingleUser.res\n// Title is now \"Someuser Name | Users | MyApp\"\ncontext.requestController-\u003eResX.RequestController.prepentTitleSegment(user.name)\n\n// There's also an `appendTitleSegment` for appending to the title\n// Title is now \"Someuser Name | Users | MyApp | Appeneded Content\"\ncontext.requestController-\u003eResX.RequestController.appendTitleSegment(\"Appeneded Content\")\n\n```\n\nIt's also easy to set the title to something else entirely with `setFullTitle`:\n\n```rescript\n// SingleUser.res\n// Title is now \"Failed!\"\ncontext.requestController-\u003eResX.RequestController.setFullTitle(\"Failed!\")\n```\n\n\u003e Note: You control how the title is rendered by passing a `renderTitle` function to `handleRequest`.\n\n### Generic append to head\n\nIt's not just `\u003ctitle\u003e` that might be inconvenient to have to produce as you're rendering `\u003chead\u003e`. You might have styles or other things that you might want to load depending on what you're rendering, and that belongs in `\u003chead\u003e`. `ResX.RequestController` comes with a generic `appendToHead` function for that:\n\n```rescript\n// SingleUser.res\ncontext.requestController-\u003eResX.RequestController.appendToHead(\u003clink href={ResXAssets.assets.single_user_page_styles_css} rel=\"text/stylesheet\" /\u003e)\n```\n\nThere's also a component you can use to render things into head. This component can be rendered anywhere in the component tree and the content will still be rendered in `\u003chead\u003e`:\n\n```rescript\n// SingleUser.res\n\u003cdiv\u003e\n  \u003cResX.RenderInHead\u003e\n    \u003clink href={ResXAssets.assets.single_user_page_styles_css} rel=\"text/stylesheet\" /\u003e\n  \u003c/ResX.RenderInHead\u003e\n\u003c/div\u003e\n```\n\n### Redirects\n\nYou can redirect easily using `ResX.RequestController.redirect`:\n\n```rescript\nrequestController-\u003eResX.RequestController.redirect(\"/start\", ~status=302)\n```\n\nThis returns a JSX element, so you can easily integrate it wherever you want to set the redirect:\n\n```rescript\nswitch path {\n| list{\"moved\"} =\u003e\n  requestController-\u003eResX.RequestController.redirect(\"/start\", ~status=302)\n```\n\n### Cache control\n\nCache headers can be a bit confusing. ResX comes with a helper to produce the cache control header string via `ResX.Utils.CacheControl`. Here's an example:\n\n```rescript\n// Sets Cache-Control to \"public, max-age=86400\"\ncontext.headers-\u003eHeaders.set(\n  \"Cache-Control\",\n  ResX.Utils.CacheControl.make(~cacheability=Public, ~expiration=[MaxAge(Days(1.))]),\n)\n```\n\nThere's also a number of cache control presets available under `ResX.Utils.CacheControl.Presets`. This includes examples for static assets that are to be cached long term, to sensitive content that should never be cached by anyone.\n\n### Response status\n\nYou can set the response status anywhere when rendering:\n\n```rescript\n// FourOhFour.res\n@jsx.component\nlet make = () =\u003e {\n  let context = ResX.Handlers.useContext(HtmxHandler.handler)\n  context.requestController-\u003eResX.RequestController.setStatus(404)\n\n  \u003cdiv\u003e {Hjsx.string(\"404\")} \u003c/div\u003e\n}\n```\n\n### Other headers\n\nSetting any other header anywhere when rendering is also easy:\n\n```rescript\nlet context = ResX.Handlers.useContext(HtmxHandler.handler)\ncontext.headers-\u003eHeaders.set(\"Content-Type\", \"text/html\")\n```\n\n### Advanced: Doc header\n\nBy default, any returned content from your handlers is prefixed with `\u003c!DOCTYPE html\u003e` because you're expected to return HTML. However, there are cases where you might want to return other things than HTML but still use JSX. Examples include returning XML to produce a site map, CSV files for data export, or other structured data formats. For that, you can leverage `ResX.RequestController.setDocHeader`:\n\n#### XML sitemap example\n\n```rescript\n// SiteMap.res\n@jsx.component\nlet make = () =\u003e {\n  let {requestController, headers} = HtmxHandler.handler-\u003eResX.Handlers.useContext\n\n  requestController-\u003eResX.RequestController.setDocHeader(\n    Some(`\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?\u003e`),\n  )\n\n  headers-\u003eHeaders.set(\"Content-Type\", \"application/xml; charset=UTF-8\")\n  headers-\u003eHeaders.set(\n    \"Cache-Control\",\n    ResX.Utils.CacheControl.make(~cacheability=Public, ~expiration=[MaxAge(Days(1.))]),\n  )\n\n  \u003curlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"\u003e\n    \u003curl\u003e\n      \u003cloc\u003e {Hjsx.string(\"https://www.example.com/\")} \u003c/loc\u003e\n      \u003clastmod\u003e {Hjsx.string(\"2023-10-15\")} \u003c/lastmod\u003e\n      \u003cchangefreq\u003e {Hjsx.string(\"weekly\")} \u003c/changefreq\u003e\n      \u003cpriority\u003e {Hjsx.string(\"1.0\")} \u003c/priority\u003e\n    \u003c/url\u003e\n  \u003c/urlset\u003e\n}\n```\n\n#### CSV export example\n\n```rescript\n// UserCsvExport.res\n@jsx.component\nlet make = async (~users: array\u003cuser\u003e) =\u003e {\n  let {requestController, headers} = HtmxHandler.handler-\u003eResX.Handlers.useContext\n\n  // Remove the HTML doctype since we're returning CSV\n  requestController-\u003eResX.RequestController.setDocHeader(None)\n\n  headers-\u003eHeaders.set(\"Content-Type\", \"text/csv; charset=UTF-8\")\n  headers-\u003eHeaders.set(\"Content-Disposition\", \"attachment; filename=\\\"users.csv\\\"\")\n\n  let csvHeader = \"Name,Email,Created At\\n\"\n  let csvRows = users\n    -\u003eArray.map(user =\u003e `${user.name},${user.email},${user.createdAt}`)\n    -\u003eArray.join(\"\\n\")\n\n  let fullCsvContent = csvHeader ++ csvRows\n\n  // Since we're outputting raw CSV content (not HTML), we need to use dangerouslyOutputUnescapedContent\n  Hjsx.dangerouslyOutputUnescapedContent(fullCsvContent)\n}\n```\n\nYou can then render these whenever someone requests the appropriate paths:\n\n```rescript\nrender: async ({path}) =\u003e {\n  switch path {\n  | list{\"sitemap.xml\"} =\u003e \u003cSiteMap /\u003e\n  | list{\"users\", \"export.csv\"} =\u003e\n    let users = await Database.getAllUsers()\n    \u003cUserCsvExport users /\u003e\n  ...\n```\n\n### Handling forms\n\n- `FormDataHelpers`\n- `FormData`\n\n### Vite plugin\n\nResX comes with its own Vite plugin that takes care of all configuration for you. It will:\n\n- Ensure all ResX assets are handled and included properly\n- Ensure that Hot Module Reloading works for all assets and that Vite dev mode is properly wired up to your local ResX dev server\n\n\u003e Note: Right now, using ResX with more elaborate Vite config than what's preconfigured for you might be problematic. This will change in the future though so that ResX is just another part of your Vite config. Open issues please when you find use cases you'd like supported but that doesn't work now.\n\n## Static site generation\n\nWIP: Static site generation is easy to do. `StaticExporter.res` and `demo/Exporter.res`.\n\n## Ideas\n\nThis section will be expanded as we go along.\n\n- Auth\n- Enhanced cookies\n- Router abstraction\n- Relay for ResX\n- Static and semi-static generation\n- Suspense and (out of order) streaming\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzth%2Fres-x","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzth%2Fres-x","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzth%2Fres-x/lists"}