Password reset is authentication's back door. Teams put effort into login - rate limiting, multi-factor, strong hashing - and then implement reset as a convenience feature, which is precisely why attackers look there. Every control on the login path is irrelevant if the reset flow will issue a working credential to someone who is not the account holder.
The token is where most implementations go wrong, and the requirements are specific. It must be generated from a cryptographically secure random source, not from a timestamp, a sequential value, or a hash of the user's email - all of which have been shipped by real products and all of which are guessable. It must be long enough to be unguessable, single-use, and short-lived: fifteen to sixty minutes is the usual range, and anything measured in days is a credential sitting in an inbox.
Store it hashed, exactly as you would a password. A reset token in the database in plaintext means anyone who reads your database - through an injection flaw, a leaked backup, an over-privileged internal account - can take over any account without needing to crack a single password hash. Hashing it costs nothing and removes that entire path.
Do not reveal whether an account exists. If requesting a reset for an unknown address returns a different message, a different status code, or noticeably faster, you have built an account enumeration tool - which is how attackers build the target list they then use for credential stuffing. Return the same response either way, take a similar amount of time, and say that if an account exists, an email has been sent.
Invalidate everything on a successful reset. All outstanding reset tokens for that account, and all existing sessions on all devices. The reason is the scenario where the reset is itself the recovery: an attacker has the account and the real owner is taking it back. If the attacker's session survives the password change, the recovery has not recovered anything, and this is the detail most frequently missed.
Two additions worth the effort. Rate-limit reset requests per account and per address, because otherwise the endpoint doubles as a way to flood someone's inbox using your domain's reputation. And notify the user by email when the password actually changes, to an address that cannot itself be changed in the same flow without confirmation - that notification is often the first thing that tells a real owner something is wrong, and it costs one email.