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(, 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(
+
+ );
+
+ responseHeaders.set("Content-Type", "text/html");
+
+ return new Response("" + 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 (
+
+