{"id":16442542,"url":"https://github.com/smeijer/remix-data-kit","last_synced_at":"2025-09-10T05:35:33.476Z","repository":{"id":238569010,"uuid":"796836886","full_name":"smeijer/remix-data-kit","owner":"smeijer","description":"a kit to simplify handling form submissions in remix","archived":false,"fork":false,"pushed_at":"2025-02-04T13:54:22.000Z","size":654,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-08-09T15:02:12.524Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","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/smeijer.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":".github/funding.yml","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,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":["smeijer"]}},"created_at":"2024-05-06T18:12:11.000Z","updated_at":"2025-02-04T13:54:26.000Z","dependencies_parsed_at":"2024-05-06T19:31:23.806Z","dependency_job_id":"409b358c-e50c-4d80-93f5-9a6f0d904c63","html_url":"https://github.com/smeijer/remix-data-kit","commit_stats":null,"previous_names":["smeijer/remix-data-kit"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/smeijer/remix-data-kit","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smeijer%2Fremix-data-kit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smeijer%2Fremix-data-kit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smeijer%2Fremix-data-kit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smeijer%2Fremix-data-kit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/smeijer","download_url":"https://codeload.github.com/smeijer/remix-data-kit/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smeijer%2Fremix-data-kit/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":274417335,"owners_count":25281108,"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-09-10T02:00:12.551Z","response_time":83,"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-11T09:17:49.815Z","updated_at":"2025-09-10T05:35:33.382Z","avatar_url":"https://github.com/smeijer.png","language":"TypeScript","funding_links":["https://github.com/sponsors/smeijer"],"categories":[],"sub_categories":[],"readme":"# remix-data-kit\n\n\u003e a kit to simplify remix actions / handling validated form data including file uploads.\n\n## Install\n\n```sh\nnpm install remix-data-kit\n```\n\n## Usage\n\nWe provide the `createActionHandler` method that creates a remix `ActionFunction`. In our actions, validated `data` is the first argument, remix `AppLoadContext` the second, and the remix default `ActionFunctionArgs` as third in case you'd really need it.\n\nThe data argument is the posted json, or FormData expanded using [form-data-kit]. Any file streams are piped to your `onFile` handler. No more manually handling FormData.\n\n```ts\nimport { createActionHandler, createAction } from 'remix-data-kit';\nimport { Static, Type } from '@sinclair/typebox';\nimport { json } from '@remix-run/node';\n\nexport const CreateCommentSchema = Type.Object({\n  body: Type.String({ minLength: 1, maxLength: 280 }),\n  author: Type.String(),\n});\n\nconst createComment = createAction({\n  // the name is used as action intent\n  name: 'create-comment',\n  // schema to validate the posted json or FormData\n  schema: CreateCommentSchema,\n  // we provide data as first arg, and the context as second for convinience\n  handler: async (data, { db }, args) =\u003e {\n    // data is validated \u0026 typed!\n    const inserted = await db.comments.insert(data);\n\n    // return a remix/react-router valid response\n    return json({ ok: true, comment: inserted });\n  },\n});\n\nexport const action = createActionHandler({\n  createComment,\n  // updateComment,\n  // deleteComment,\n});\n```\n\nWithout `remix-data-kit` it would look something like this:\n\n```ts\nexport const action = async ({ request, context }: ActionFunctionArgs) =\u003e {\n  const formData = await request.formData();\n  const _intent = formData.get('_intent');\n\n  switch (_intent) {\n    case 'create-comment': {\n      assertUser(request);\n      const body = formData.get('body').trim();\n      const author = formData.get('author');\n\n      if (body.length \u003c 1 || body.length \u003e 280) {\n        throw json({ msg: 'invalid' }, { status: 422 });\n      }\n\n      const inserted = await db.comments.insert({ author, body });\n      return json({ ok: true, comment: inserted });\n    }\n\n    case 'update-comment': {\n      // ...\n    }\n\n    case 'delete-comment': {\n      // ...\n    }\n\n    default: {\n      throw json({ ok: false, message: `No handler found for action: ${_intent}` }, { status: 404 });\n    }\n  }\n};\n```\n\n## Intent\n\nWe can't submit the forms without an intent, so let's handle that first. To make `remix-data-kit` understand your action intent, add one of the intent keys to your form `action` attribute:\n\n```tsx\n\u003cform method=\"post\" action=\"?/create-comment\"\u003e\n\u003cform method=\"post\" action=\"?action=create-comment\"\u003e\n\u003cform method=\"post\" action=\"?intent=create-comment\"\u003e\n```\n\nIt's a popular convention to use a named submit button, instead of this action url, but adding the intent to the url, allows us to extract the intent, before the full body is received, parsed, and validated. This way we can return a 404 before say all file uploads are processed.\n\n## Validation\n\nTo ease validation, we're providing a [typebox] schema to our createAction method. Under the hood, we're using the `assertType` utility from [typebox-assert] to assert and narrow the type, and have wrapped that to throw a response instead of Error, that asserts and narrows the type of the submitted data.\n\n`assertType` throws a `Response` with errors when the type is invalid. It also mutates the object to:\n\n- remove additional properties that are not defined in the schema\n- add missing properties by using schema defaults\n- cast property types where possible\n\n```ts\nimport { createActionHandler, createAction, assertType } from 'remix-data-kit';\nimport { Type, Static } from '@sinclair/typebox';\n\nconst CreateComment = Type.Object({\n  body: Type.String(),\n  tags: Type.String(),\n});\n\nexport const action = createActionHandler({\n  createComment: createAction({\n    schema: CreateComment,\n    handler: async (data) =\u003e {\n      // data is typed as Static\u003cCreateComment\u003e\n    },\n  }),\n});\n\n// this would be the same:\nexport const action = createActionHandler({\n  createComment: createAction({\n    handler: async (data: unknown) =\u003e {\n      assertType(CreateComment, data, 'data is not a valid comment');\n      // data is narrowed to Static\u003cCreateComment\u003e\n    },\n  }),\n});\n```\n\nOur recommendation is to use the `schema` property for actions, while using `assertType` to verify data structured from data that you fetch inside the actions, from say third party services.\n\n## Expansion\n\nWe use [form-data-kit] to expand form data. Meaning, the data on your server is a structured json object even when form fields themselves are flat. For example:\n\n```tsx\n\u003cinput name=\"user.name\" value=\"Alex\" /\u003e\n\u003cinput name=\"user.handle\" value=\"@example\" /\u003e\n\u003cinput name=\"colors[].label\" value=\"blue\" /\u003e\n\u003cinput name=\"colors[].label\" value=\"red\" /\u003e\n\u003cinput name=\"colors[].label\" value=\"green\" /\u003e\n```\n\nMaps into:\n\n```ts\nconst data = {\n  user: { name: 'Alex', handle: '@example' },\n  colors: [{ label: 'blue' }, { label: 'red' }, { label: 'green' }],\n};\n```\n\n## File uploads\n\nFile uploads are handled by simply adding an `onFile` handler. Write the file to disk, or stream\nit to another service provider. By the time all files are handled, the remaining payload is validated\nand the `handler` is called to return a response.\n\n```ts\nimport { assertType, readableStreamToFile } from 'remix-data-kit';\n\nexport const AttachmentSchema = Type.Object({\n  id: Type.String(),\n  files: Type.Array(\n    Type.Object({\n      id: Type.Number(),\n      name: Type.String(),\n      url: Type.String(),\n    }),\n  ),\n});\n\nexport const createAttachmentAction = createAction({\n  name: 'create-attachment',\n  schema: AttachmentSchema,\n  handler: (data) =\u003e {\n    // here, data is of type Static\u003cAttachmentSchema\u003e, and `files` is\n    // no longer a blob, but the meta data from the files\n    return json({ ok: true, data });\n  },\n  // on file runs for every file upload (blob in FormData), and before handler\n  onFile: async ({ file, info }) =\u003e {\n    const blob = await uploadToS3({ file, info });\n    // assign some data to `info` to make it available in `handler`,\n    // be sure to match your schema type\n    info.id = blob.id;\n    info.url = blob.url;\n  },\n});\n```\n\n## Stream utilities\n\nWe're providing two stream utilities to make it easier to deal with file uploads.\n\n**readableStreamToFile**, converts a ReadableStream to a File\n**readableStreamToBlob**, converts a ReadableStream to a Blob\n\nThese methods make it trivial to map the stream into a format that can be provided to say FormData.\n\nThe `onFile` handler provides you with a Web ReadableStream, remember to use `Readable.fromWeb` if you'd need a Node stream.\n\n## Limits\n\nWe support a couple of limits to manage how big a posted json object could be, or to accept only a certain file count of file type. The currently supported limits are:\n\n- **fileCount** _number_\n\n  The maximum number of files a user can upload. Note that empty file fields, still count against the file count limit.\n\n- **fileSize** _number | string_\n\n  The maximum size per file in bytes.\n\n- **fieldSize** _number | string_;\n\n  The maximum size of text fields.\n\n- **jsonSize** _number | string_;\n\n  The maximum size of json payloads.\n\n- **mimeType** _string | string[]_;\n\n  A valid [HTML accept](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept) string to restrict mime-types\n\nProvide these to your action's limits property:\n\n```ts\nexport const createAttachmentAction = createAction({\n  name: 'create-attachment',\n  schema: AttachmentSchema,\n  handler: async (data) =\u003e {\n    /* ... */\n  },\n  onFile: async ({ file, info }) =\u003e {\n    /* ... */\n  },\n  limits: {\n    fileCount: 3,\n    fileSize: '1mb',\n    fieldSize: '10kb',\n    jsonSize: '150kb',\n    mimeType: ['image/png', 'image/jpg'],\n  },\n});\n```\n\n## Content-Type\n\nWhen using `createAction`, your endpoint suddenly supports not only FormData, but also json. Post using any of the content types:\n\n- application/json\n- application/x-www-form-urlencoded\n- multipart/form-data\n\n[typebox-assert]: https://npmjs.com/typebox-assert\n[form-data-kit]: https://npmjs.com/form-data-kit\n[typebox]: https://github.com/sinclairzx81/typebox\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmeijer%2Fremix-data-kit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsmeijer%2Fremix-data-kit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmeijer%2Fremix-data-kit/lists"}