diff --git a/contributors.yml b/contributors.yml
index f609931122b..4a1b02b1832 100644
--- a/contributors.yml
+++ b/contributors.yml
@@ -129,6 +129,7 @@
 - gonzoscript
 - graham42
 - GregBrimble
+- GSt4r
 - guatedude2
 - guerra08
 - gunners6518
diff --git a/examples/file-and-s3-upload/.env.sample b/examples/file-and-s3-upload/.env.sample
new file mode 100644
index 00000000000..17b368d399e
--- /dev/null
+++ b/examples/file-and-s3-upload/.env.sample
@@ -0,0 +1,4 @@
+STORAGE_ACCESS_KEY=
+STORAGE_SECRET=
+STORAGE_REGION=
+STORAGE_BUCKET=
\ No newline at end of file
diff --git a/examples/file-and-s3-upload/.eslintrc.js b/examples/file-and-s3-upload/.eslintrc.js
new file mode 100644
index 00000000000..ced78085f86
--- /dev/null
+++ b/examples/file-and-s3-upload/.eslintrc.js
@@ -0,0 +1,3 @@
+module.exports = {
+  extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"],
+};
diff --git a/examples/file-and-s3-upload/.gitignore b/examples/file-and-s3-upload/.gitignore
new file mode 100644
index 00000000000..3f7bf98da3e
--- /dev/null
+++ b/examples/file-and-s3-upload/.gitignore
@@ -0,0 +1,6 @@
+node_modules
+
+/.cache
+/build
+/public/build
+.env
diff --git a/examples/file-and-s3-upload/README.md b/examples/file-and-s3-upload/README.md
new file mode 100644
index 00000000000..36249dcd670
--- /dev/null
+++ b/examples/file-and-s3-upload/README.md
@@ -0,0 +1,35 @@
+# Upload images to S3
+
+This is a simple example of using the remix built-in [uploadHandler](https://remix.run/docs/en/v1/api/remix#uploadhandler) and Form with multipart data to upload a file with the built-in local uploader and upload an image file to S3 with a custom uploader and display it. You can test it locally by running the dev server and opening the path `/s3-upload` in your browser.
+
+The relevent files are:
+
+```
+├── app
+│   ├── routes
+│   │   ├── s3-upload.tsx // upload to S3
+│   └── utils
+│       └── s3.server.ts  // init S3 client on server side
+|── .env // holds AWS S3 credentails
+```
+
+## Steps to set up an S3 bucket
+
+- Sign up for an [AWS account](https://portal.aws.amazon.com/billing/signup) - this will require a credit card
+- Create an S3 bucket in your desired region
+- Create an access key pair for an IAM user that has access to the bucket
+- Copy the .env.sample to .env and fill in the S3 bucket, the region as well as the access key and secret key from the IAM user
+
+Note: in order for the image to be displayed after being uploaded to your S3 bucket in this example, the bucket needs to have public access enabled, which is potentially dangerous. 
+
+> :warning: Lambda imposes a [limit of 6MB](https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html) on the invocation payload size. If you use this example with Remix running on Lambda, you can only update files with a size smaller than 6MB.
+
+Open this example on [CodeSandbox](https://codesandbox.com):
+
+[![Open in CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/remix-run/remix/tree/main/examples/file-and-s3-upload)
+
+## Related Links
+
+- [Handle Multiple Part Forms (File Uploads)](https://remix.run/docs/en/v1/api/remix#unstable_parsemultipartformdata-node)
+- [Upload Handler](https://remix.run/docs/en/v1/api/remix#uploadhandler)
+- [Custom Uploader](https://remix.run/docs/en/v1/api/remix#custom-uploadhandler)
diff --git a/examples/file-and-s3-upload/app/entry.client.tsx b/examples/file-and-s3-upload/app/entry.client.tsx
new file mode 100644
index 00000000000..3eec1fd0a02
--- /dev/null
+++ b/examples/file-and-s3-upload/app/entry.client.tsx
@@ -0,0 +1,4 @@
+import { RemixBrowser } from "@remix-run/react";
+import { hydrate } from "react-dom";
+
+hydrate(<RemixBrowser />, document);
diff --git a/examples/file-and-s3-upload/app/entry.server.tsx b/examples/file-and-s3-upload/app/entry.server.tsx
new file mode 100644
index 00000000000..5afa18235cc
--- /dev/null
+++ b/examples/file-and-s3-upload/app/entry.server.tsx
@@ -0,0 +1,21 @@
+import type { EntryContext } from "@remix-run/node";
+import { RemixServer } from "@remix-run/react";
+import { renderToString } from "react-dom/server";
+
+export default function handleRequest(
+  request: Request,
+  responseStatusCode: number,
+  responseHeaders: Headers,
+  remixContext: EntryContext
+) {
+  const markup = renderToString(
+    <RemixServer context={remixContext} url={request.url} />
+  );
+
+  responseHeaders.set("Content-Type", "text/html");
+
+  return new Response("<!DOCTYPE html>" + markup, {
+    status: responseStatusCode,
+    headers: responseHeaders,
+  });
+}
diff --git a/examples/file-and-s3-upload/app/root.tsx b/examples/file-and-s3-upload/app/root.tsx
new file mode 100644
index 00000000000..927a0f745df
--- /dev/null
+++ b/examples/file-and-s3-upload/app/root.tsx
@@ -0,0 +1,32 @@
+import type { MetaFunction } from "@remix-run/node";
+import {
+  Links,
+  LiveReload,
+  Meta,
+  Outlet,
+  Scripts,
+  ScrollRestoration,
+} from "@remix-run/react";
+
+export const meta: MetaFunction = () => ({
+  charset: "utf-8",
+  title: "New Remix App",
+  viewport: "width=device-width,initial-scale=1",
+});
+
+export default function App() {
+  return (
+    <html lang="en">
+      <head>
+        <Meta />
+        <Links />
+      </head>
+      <body>
+        <Outlet />
+        <ScrollRestoration />
+        <Scripts />
+        <LiveReload />
+      </body>
+    </html>
+  );
+}
diff --git a/examples/file-and-s3-upload/app/routes/s3-upload.tsx b/examples/file-and-s3-upload/app/routes/s3-upload.tsx
new file mode 100644
index 00000000000..e4dc02d8b1b
--- /dev/null
+++ b/examples/file-and-s3-upload/app/routes/s3-upload.tsx
@@ -0,0 +1,60 @@
+import type { ActionFunction, UploadHandler } from "@remix-run/node";
+import {
+    json,
+  unstable_composeUploadHandlers as composeUploadHandlers,
+  unstable_createMemoryUploadHandler as createMemoryUploadHandler,
+  unstable_parseMultipartFormData as parseMultipartFormData,
+} from "@remix-run/node";
+import { useFetcher } from "@remix-run/react";
+import { s3UploadHandler } from "~/utils/s3.server";
+
+type ActionData = {
+  errorMsg?: string;
+  imgSrc?: string;
+  imgDesc?: string;
+};
+
+export const action: ActionFunction = async ({ request }) => {
+  const uploadHandler: UploadHandler = composeUploadHandlers(
+    s3UploadHandler,
+    createMemoryUploadHandler()
+  );
+  const formData = await parseMultipartFormData(request, uploadHandler);
+  const imgSrc = formData.get("img");
+  const imgDesc = formData.get("desc");
+  console.log(imgDesc)
+  if (!imgSrc) {
+    return json({
+      errorMsg: "Something went wrong while uploading",
+    });
+  }
+  return json({
+    imgSrc,
+    imgDesc,
+  });
+};
+
+export default function Index() {
+  const fetcher = useFetcher<ActionData>();
+  return (
+    <>
+      <fetcher.Form method="post" encType="multipart/form-data">
+        <label htmlFor="img-field">Image to upload</label>
+        <input id="img-field" type="file" name="img" accept="image/*" />
+        <label htmlFor="img-desc">Image description</label>
+        <input id="img-desc" type="text" name="desc" />
+        <button type="submit">Upload to S3</button>
+      </fetcher.Form>
+      {fetcher.type === "done" ? (
+        fetcher.data.errorMsg ? (
+          <h2>{fetcher.data.errorMsg}</h2>
+        ) : (
+         <>
+          <div>File has been uploaded to S3 and is available under the following URL (if the bucket has public access enabled):</div>
+          <div>{fetcher.data.imgSrc}</div>
+          <img src={fetcher.data.imgSrc} alt={fetcher.data.imgDesc || "Uploaded image from S3"} />
+        </>
+      )) : null}
+    </>
+  );
+}
diff --git a/examples/file-and-s3-upload/app/utils/s3.server.ts b/examples/file-and-s3-upload/app/utils/s3.server.ts
new file mode 100644
index 00000000000..5f755c83fff
--- /dev/null
+++ b/examples/file-and-s3-upload/app/utils/s3.server.ts
@@ -0,0 +1,46 @@
+import AWS from "aws-sdk"
+import type { UploadHandler } from "@remix-run/node"
+import { writeAsyncIterableToWritable } from "@remix-run/node"
+import { PassThrough } from "stream"
+
+const { STORAGE_ACCESS_KEY, STORAGE_SECRET, STORAGE_REGION, STORAGE_BUCKET } = process.env
+
+if (!(STORAGE_ACCESS_KEY && STORAGE_SECRET && STORAGE_REGION && STORAGE_BUCKET)) {
+  throw new Error(`Storage is missing required configuration.`)
+}
+
+const uploadStream = ({ Key }: Pick<AWS.S3.Types.PutObjectRequest, 'Key'>) => {
+  const s3 = new AWS.S3({
+    credentials: {
+      accessKeyId: STORAGE_ACCESS_KEY,
+      secretAccessKey: STORAGE_SECRET,
+    },
+    region: STORAGE_REGION,
+  })
+  const pass = new PassThrough()
+  return {
+    writeStream: pass,
+    promise: s3.upload({ Bucket: STORAGE_BUCKET, Key, Body: pass }).promise(),
+  }
+}
+
+export async function uploadStreamToS3(data: any, filename: string) {
+  const stream = uploadStream({
+    Key: filename,
+  })
+  await writeAsyncIterableToWritable(data, stream.writeStream)
+  const file = await stream.promise
+  return file.Location
+}
+
+export const s3UploadHandler: UploadHandler = async ({
+  name,
+  filename,
+  data,
+}) => {
+  if (name !== "img") {
+    return undefined;
+  }
+  const uploadedFileLocation = await uploadStreamToS3(data, filename!)
+  return uploadedFileLocation
+}
diff --git a/examples/file-and-s3-upload/package.json b/examples/file-and-s3-upload/package.json
new file mode 100644
index 00000000000..edd814eb782
--- /dev/null
+++ b/examples/file-and-s3-upload/package.json
@@ -0,0 +1,28 @@
+{
+  "private": true,
+  "sideEffects": false,
+  "scripts": {
+    "build": "remix build",
+    "dev": "remix dev",
+    "start": "remix-serve build"
+  },
+  "dependencies": {
+    "@remix-run/node": "1.5.1",
+    "@remix-run/react": "1.5.1",
+    "@remix-run/serve": "1.5.1",
+    "aws-sdk": "^2.1152.0",
+    "react": "^17.0.2",
+    "react-dom": "^17.0.2"
+  },
+  "devDependencies": {
+    "@remix-run/dev": "1.5.1",
+    "@remix-run/eslint-config": "1.5.1",
+    "@types/react": "^17.0.39",
+    "@types/react-dom": "^17.0.13",
+    "eslint": "^8.10.0",
+    "typescript": "^4.6.2"
+  },
+  "engines": {
+    "node": ">=14"
+  }
+}
diff --git a/examples/file-and-s3-upload/public/favicon.ico b/examples/file-and-s3-upload/public/favicon.ico
new file mode 100644
index 00000000000..8830cf6821b
Binary files /dev/null and b/examples/file-and-s3-upload/public/favicon.ico differ
diff --git a/examples/file-and-s3-upload/remix.config.js b/examples/file-and-s3-upload/remix.config.js
new file mode 100644
index 00000000000..260b82c7cb1
--- /dev/null
+++ b/examples/file-and-s3-upload/remix.config.js
@@ -0,0 +1,10 @@
+/**
+ * @type {import('@remix-run/dev').AppConfig}
+ */
+module.exports = {
+  ignoredRouteFiles: ["**/.*"],
+  // appDirectory: "app",
+  // assetsBuildDirectory: "public/build",
+  // serverBuildPath: "build/index.js",
+  // publicPath: "/build/",
+};
diff --git a/examples/file-and-s3-upload/remix.env.d.ts b/examples/file-and-s3-upload/remix.env.d.ts
new file mode 100644
index 00000000000..72e2affe311
--- /dev/null
+++ b/examples/file-and-s3-upload/remix.env.d.ts
@@ -0,0 +1,2 @@
+/// <reference types="@remix-run/dev" />
+/// <reference types="@remix-run/node/globals" />
diff --git a/examples/file-and-s3-upload/sandbox.config.json b/examples/file-and-s3-upload/sandbox.config.json
new file mode 100644
index 00000000000..4363d87a30d
--- /dev/null
+++ b/examples/file-and-s3-upload/sandbox.config.json
@@ -0,0 +1,6 @@
+{
+  "hardReloadOnChange": true,
+  "container": {
+    "port": 3000
+  }
+}
diff --git a/examples/file-and-s3-upload/tsconfig.json b/examples/file-and-s3-upload/tsconfig.json
new file mode 100644
index 00000000000..20f8a386a6c
--- /dev/null
+++ b/examples/file-and-s3-upload/tsconfig.json
@@ -0,0 +1,22 @@
+{
+  "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
+  "compilerOptions": {
+    "lib": ["DOM", "DOM.Iterable", "ES2019"],
+    "isolatedModules": true,
+    "esModuleInterop": true,
+    "jsx": "react-jsx",
+    "moduleResolution": "node",
+    "resolveJsonModule": true,
+    "target": "ES2019",
+    "strict": true,
+    "allowJs": true,
+    "forceConsistentCasingInFileNames": true,
+    "baseUrl": ".",
+    "paths": {
+      "~/*": ["./app/*"]
+    },
+
+    // Remix takes care of building everything in `remix build`.
+    "noEmit": true
+  }
+}