Understanding when to use String vs str

A String type is a container for a str that is stored on the heap.

Exactly correct.

str is simply a reference and will most commonly be seen as &str.

&str is a reference, yes, but str is not. str is the actual text itself, no kind of pointer involved. The reason for all the complications is that str is intrinsically variable-sized, which makes it hard to work with, but when you do, str is definitely not a reference.

(An example of where you might use neither String nor &str is Arc<str>, a reference-counted string. If you don't know what that means yet, don't worry about it.)

The heuristic I have in my head is that a String type should only be seen as part of the type that owns it. For example, as part of a struct definition.

When being passed into methods, or returning data from a method, then use an &str type as this will work as a reference to the original ownership.

Generally, you should use &str in function arguments and returns when you can use &str, but you can't always. In particular, any function which creates a new string that did not previously exist must return String rather than &str, because in order to continue existing past the function returning, the string needs to be owned by the return value.

The way I would suggest looking at it is: use &str when you know there is an owner of the string already, and they will hold still for you to borrow it as long as you need it. If there is no existing owner, or if the owner has its own business that is incompatible with you borrowing it, then you need to use String.