When clients upload files directly to S3, your API avoids handling large payloads and you get better scalability. I generate a presigned PUT URL with a short expiry and a constrained object key prefix so users can’t overwrite arbitrary objects. The crucial part is that the backend owns the key: the client asks for an upload, the server returns {key, url}, and the client uploads bytes to S3. Then the client notifies the backend to “finalize” the upload. This two-step flow prevents a class of spoofing bugs where a client claims it uploaded something it didn’t. In production I also include expected content type and size limits in policy (for POST) or enforce them at finalize-time. Presigning is simple, but the workflow around it is where security lives.