Prerequisites

  • Node.js
  • npm, pnpm, or yarn (Bun recommend)
  • AWS account with S3 bucket access

Step 1: Install Astro

Open a terminal and run:

npm create astro@latest s3-image-upload-api

Follow the interactive setup.Where will be asked for the project configuration, once done. Navigate into your project:

cd s3-image-upload-api

Start the development server:

npm run dev

If something went wrong, please follow the official Astro docs for installing. Here Install Astro


Step 2: Install AWS SDK and Configure S3 Client

Install the AWS SDK:

npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner

Create an .env file in your project root and add:

AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
AWS_REGION=your-region
AWS_BUCKET_NAME=your-bucket

Create an s3Client.js file inside lib/:

import { S3Client } from "@aws-sdk/client-s3";

export const s3Client = new S3Client({
   region: process.env.AWS_REGION,
   credentials: {
      accessKeyId: process.env.AWS_ACCESS_KEY_ID,
      secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
   },
});

Step 3: Generate a Signed URL

Create a function to generate signed URLs

import { GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { s3Client } from "./s3Client";

export async function getSignedUrlForFile(fileName) {
   const command = new GetObjectCommand({
      Bucket: process.env.AWS_BUCKET_NAME,
      Key: fileName,
   });
   return await getSignedUrl(s3Client, command, { expiresIn: 3600 });
}

Step 4: Generate a Seting up endpoints

export const POST: APIRoute = async ({ request }) => {
    const formData = await request.formData();
    const file = formData.get("image") as File;

    if (!file) {
        return new Response(JSON.stringify({ error: "No file uploaded" }), {
            status: 400,
            headers: { "Content-Type": "application/json" },
        });
    }

    const uniquePrefix = Date.now() + "-" + Math.round(Math.random() * 1e9)
    const fileExtension = file.name.split(".").pop()
    const fileName = `${uniquePrefix}.${fileExtension}`

    const bytes = await file.arrayBuffer()
    const fileBuffer = Buffer.from(bytes)

    const putObjectParams = {
        Bucket: process.env.AWS_BUCKET_NAME!,
        Key: fileName,
        Body: fileBuffer,
        ContentType: file.type,
    };

    const command = new PutObjectCommand(putObjectParams);
    const s3UploadResponse = await s3Client.send(command);

    /* IMPORTANT: We are saving only a reference to the image (the S3 object key).
    Since this is a private S3 bucket, you must configure your S3 bucket for 
    public or general access if you want to use direct URLs.

    Alternatively, you can generate a pre-signed URL to grant temporary access.

    Here, we handle a private S3 bucket, but since we define a limited time 
    for the valid link, it allows temporary access to the file. */

    // const publicS3Url = await getPresignedUrl(fileName);

    // Save only the S3 object key for future queries.

    return new Response(JSON.stringify({ path: fileName }), {
        status: 200,
        headers: { "Content-Type": "application/json" },
    });

In Amazon S3, the term KEY refers to the resource's unique identifier, which is essentially the name of the file within the bucket. Here, we save this key for future queries.


Step 5: Use Signed URLs in Astro

Use the API in your Astro page:

---
let signedUrl = "";
const res = await fetch("/api/signed-url");
const data = await res.json();
signedUrl = data.url;
---

<a href={signedUrl} download>Download Filea>

Lastly if you need to consume the file or serve the url can call like this having a private buket:

export const GET: APIRoute = async ({ request }) => {
    const key = new URL(request.url).searchParams.get('key')!;

    if (!key) {
        return new Response(JSON.stringify({
            status: 400,
            message: 'Missing key'
        }))

    }

    try {
        const srcFile = await getPresignedUrl(key)
        return new Response(JSON.stringify({
            srcFile
        }))
    } catch (error) {
        return new Response(JSON.stringify({
            status: 500,
            message: `Error generating URL: ${error}`
        }))
    }
}

Conclusion

So if you followed the steps, you should able to see a link to your images and use it in your front end, the code is a messy right now maybe later will give a check. 🎉