Upload file to S3 Bucket with NextJS using Server Actions
Uploading files to an S3 bucket in a Next.js application can be streamlined using Server Actions, a powerful feature that enables handling server-side logic directly within your components. This guide walks you through setting up file uploads with Next.js, Server Actions, and AWS S3.
State variables we need
const fileInputRef = useRef<HTMLInputElement>(null);
const [isLoading, setIsLoading] = useState(false);
Our button (from Shadcn)
<Button
disabled={isLoading}
className='w-full justify-start space-x-2'
onClick={() => fileInputRef.current?.click()}
>
<FileUp size={18} />
<span>Subir resultados</span>
<Input
ref={fileInputRef}
disabled={isLoading}
className='hidden'
type='file'
accept='application/pdf' placeholder='Resultados'
id='upload-results'
onChange={async (event) => {
await handleResultsUpload(event, est)
}} />
</Button>
The handler, route: `order-${estimate.id}/result` is the Bucket where our file will be
const handleResultsUpload = async (event: React.ChangeEvent<HTMLInputElement>, estimate: Estimate) => {
try {
setIsLoading(true)
if (event.target.files) {
const file = event.target.files[0];
const data = new FormData()
data.append('file', file, 'resultados.pdf')
const response = await uploadFileS3({
route: `order-${estimate.id}/result`,
fileName: estimate.qrId,
content: data,
})
response.ok ? toast.success('Resultados guardados') : toast.error('Ocurrio un error al guardar los resultados')
setIsLoading(false)
}
} catch (error) {
toast.error('Ocurrio un error insesperado')
} finally {
setIsLoading(false)
}
}
The server action that will upload the file to our Bucket
'use server';
import { S3Client, PutObjectCommand, ListObjectsCommand, GetObjectCommand } from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { parseError } from '../utils';
const s3Client = new S3Client({
region: process.env.AWS_REGION || '',
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID || '',
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '',
},
})
export const uploadFileS3 = async ({
route,
fileName,
content
}: {
route: String,
fileName: String,
content: FormData
}) => {
try {
let file = content.get('file') as File
const arrayBuffer = (await file.arrayBuffer())
const buffer = Buffer.from(arrayBuffer)
if (!file) throw 'No se encontro el archivo para subir.'
const s3Client = new S3Client({
region: process.env.AWS_REGION || '',
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID || '',
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '',
},
})
const command = new PutObjectCommand({
Bucket: process.env.AWS_S3_BUCKET_NAME || '',
Key: `${route}/${fileName}`,
ContentType: 'application/pdf',
Body: buffer
})
const response = await s3Client.send(command)
return {
ok: response.$metadata.httpStatusCode === 200,
}
} catch (error) {
console.trace(error)
return {
ok: false
}
}
}
The final result
And if we check our bucket in AWS, we will have our file with a directory like structure
To download our file we would need to allow public access to our Bucket, I would not recommend doing this, I preffer to get a signed url so that we can download our file
This is the server action
'use server';
...
export const getObjectSignedUrl = async ({ key }: { key: string }) => {
const params = {
Bucket: process.env.AWS_S3_BUCKET_NAME,
Key: key,
};
try {
const command = new GetObjectCommand(params);
// this will throw an exeption if fails
const response = await s3Client.send(command)
const url = await getSignedUrl(s3Client, command, { expiresIn: 3600 });
return {
ok: true,
data: url
}
} catch (error) {
parseError(error)
return {
ok: false,
error: 'Ocurrio un error al obtener el documento desde S3'
}
}
}
In our fronted we can have, the key would be like:
order-1440/result/ae4d3845-3331-44e1-9cbf-f91716f3598e
const handleDownload = async ({
objectKey,
}: {
objectKey: string,
}) => {
try {
const signedUrl = await getObjectSignedUrl({ key: objectKey })
if (!signedUrl.ok || !signedUrl.data) {
toast.error('Error al descargar el objeto')
return;
}
window.open(signedUrl.data, '_blank');
} catch (error) {
console.log(error)
toast.error('Ocurrio un error al descargar')
} finally {
}
}
Server action to list our objects
'use server';
...
export const listObjectsS3 = async ({
route,
}: {
route: String,
}) => {
try {
const command = new ListObjectsCommand({
Bucket: process.env.AWS_S3_BUCKET_NAME || '',
Prefix: `${route}`,
Delimiter: '/'
})
const response = await s3Client.send(command)
return {
ok: response.$metadata.httpStatusCode === 200,
data: response.Contents?.map(content => content.Key)
}
} catch (error) {
console.trace(error)
return {
ok: false
}
}
}
In our fronted we can get the objects with, this just gets the first object
const getObjectsS3 = async () => {
try {
setIsLoading(true)
const route = `order-${estimateId}/result/`
const objectsS3 = await listObjectsS3({ route })
if (!objectsS3.ok) return;
if (!objectsS3.data?.length) return;
setFiles([...objectsS3.data])
} catch (error) {
} finally {
setIsLoading(false)
}
}