Upload files to s3 with express and react
Bucket #
CORS (change allowed origins, at least!):
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"POST",
"GET",
"PUT",
"DELETE",
"HEAD"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": [
"ETag"
]
}
]
I had to unblock public access (on development)
Express server #
A service to create a signed url:
api/service.ts
:
import * as z from "zod";
import { v4 as uuid } from "uuid";
import AWS from "aws-sdk";
const bucket = process.env.AWS_BUCKET;
const s3 = new AWS.S3({
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
region: process.env.AWS_REGION,
});
const RequestCreateUploadUrlSchema = z.object({
key: z.string(),
contentType: z.string(),
});
export type RequestCreateUploadUrl = z.infer<
typeof RequestCreateUploadUrlSchema
>;
export async function createUploadUrl(data: any) {
const { key, contentType } = RequestCreateUploadUrlSchema.parse(data);
const url = s3.getSignedUrl("putObject", {
Bucket: bucket,
Key: key,
ContentType: contentType,
});
return {
url,
key,
};
}
React upload #
async function createUploadUrl(
fileName: string,
fileType: string
): Promise<string> {
const key = `public/${fileName}`;
const result = await ky
.post(`${URL}/uploads`, {
json: { key, fileType },
})
.json();
return (result as any).url as string;
}
async function upload() {
const s3Url = await createUploadUrl("files/my-video.mp4", "video/mp4");
const blob = await ky.get(mediaBlobUrl).blob();
const file = new File([blob], "my-video.mp4", {
type: "video/mp4",
});
const response = await ky.put(s3Url, { body: file });
return response;
}
Express boilerplate #
api/routes.ts
:
import { Router } from "express";
import { createUploadUrl } from "./service";
const routes = Router();
routes.post("/uploads", (req, res, next) =>
createUploadUrl(req.body)
.then((response) => res.json(response))
.catch(next)
);
export default routes;
api/app.ts
:
import express, { Application, Request, Response, NextFunction } from "express";
import cors from "cors";
import helmet from "helmet";
import bodyParser from "body-parser";
import { CORS_ORIGIN, isDevelopment } from "./config";
import routes from "./routes";
const app: Application = express();
app.use(helmet());
app.use(
cors({
origin: CORS_ORIGIN,
})
);
app.use(bodyParser.json());
app.use("/", routes);
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
if (isDevelopment) {
console.error("ERROR", err.toString());
console.error("STACK", err.stack);
}
res.status(err.status || 500).send(err.toString());
});
export default app;
api/config.ts
:
import dotenv from "dotenv";
dotenv.config();
const NODE_ENV = process.env.NODE_ENV;
export const ENV =
NODE_ENV === "production"
? "production"
: NODE_ENV === "test"
? "test"
: "development";
export const CORS_ORIGIN = process.env.CORS_ORIGIN || "*";
export const PORT = parseInt(process.env.PORT || "3000");
export const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID || "";
export const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY || "";
export const AWS_REGION = process.env.AWS_REGION || "";
export const AWS_BUCKET = process.env.AWS_BUCKET || "";
api/server.ts
:
import http from "http";
import app from "./app";
import { PORT } from "./config";
const server = http.createServer(app);
server.listen(PORT, () => {
return console.log(`Server is listening on http://localhost:${PORT}`);
});
process.on("SIGTERM", () => {
console.log("Closing...");
server.close(() => {
console.log(`Bye`);
});
});