Mục lục

Upload File & Ảnh: Từ Preview đến Server S3

Hướng dẫn làm tính năng Upload Avatar: Client-side Preview (URL.createObjectURL), Validate file type/size với Zod, và Upload lên Cloud (S3/R2).

Upload file trong Next.js App Router hơi khác một chút so với Pages Router vì chúng ta dùng Server Actions.

1. Client-side Preview (UX là vua)

Đừng bắt user chờ upload xong mới thấy ảnh. Hãy hiện ảnh ngay khi họ chọn file.

tsx:
function AvatarUpload() {
  const [preview, setPreview] = useState<string | null>(null);
  const { register } = useForm();

  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (file) {
      // 🚀 Tạo URL tạm thời để hiện ảnh ngay lập tức
      const objectUrl = URL.createObjectURL(file);
      setPreview(objectUrl);
      
      // Cleanup để tránh leak memory
      return () => URL.revokeObjectURL(objectUrl);
    }
  };

  return (
    <div>
      {preview && <img src={preview} className="w-20 h-20 rounded-full" />}
      <input 
        type="file" 
        accept="image/*"
        {...register("avatar")}
        onChange={handleFileChange} 
      />
    </div>
  );
}

2. Validate File với Zod

Zod có thẻ validate file instance cực mạnh.

ts:
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
const ACCEPTED_IMAGE_TYPES = ["image/jpeg", "image/jpg", "image/png", "image/webp"];

const UploadSchema = z.object({
  avatar: z
    .any()
    .refine((files) => files?.length == 1, "Image is required.")
    .refine((files) => files?.[0]?.size <= MAX_FILE_SIZE, `Max file size is 5MB.`)
    .refine(
      (files) => ACCEPTED_IMAGE_TYPES.includes(files?.[0]?.type),
      "Only .jpg, .jpeg, .png and .webp formats are supported."
    ),
});

3. Server Action Upload (S3/R2)

Lưu ý: Bạn KHÔNG THỂ lưu file vào folder public khi deploy lên Vercel (Serverless Read-only). Bạn phải dùng dịch vụ lưu trữ ngoài (AWS S3, Cloudflare R2, Uploadthing).

tsx:
// actions.ts
'use server';
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";

const s3 = new S3Client({ ... });

export async function uploadAvatar(formData: FormData) {
  const file = formData.get("avatar") as File;
  
  // Convert File -> Buffer
  const arrayBuffer = await file.arrayBuffer();
  const buffer = Buffer.from(arrayBuffer);

  // Upload lên S3
  await s3.send(new PutObjectCommand({
    Bucket: "my-bucket",
    Key: `avatars/${Date.now()}-${file.name}`,
    Body: buffer,
    ContentType: file.type,
  }));

  // Lưu URL vào DB...
}

Kết luận

  1. Dùng URL.createObjectURL để preview ngay lập tức.
  2. Dùng Zod để chặn file quá to ngay từ Client.
  3. Server Action xử lý File -> Buffer -> S3.
Quảng cáo
mdhorizontal