Recommended resources
The freely available resources this lesson will direct you to.
Hello! Last lesson, you used &str to view UTF-8 text without copying it. The next question is an API-design question: when a function accepts or returns text, should its signature say String or &str?
For the small text-processing functions you’ll use in a backend, the answer depends on who needs to own the text, not merely whether the text came from a String. By the end of this lesson, you’ll be able to choose an input and return type deliberately—and explain why &String usually makes an interface less useful.
Read access is not ownership
A String owns its text buffer. A &str borrows a view of UTF-8 text, which may be part of a String or a string literal. The distinction from last lesson becomes especially useful at a function boundary: a function that only reads its argument need not take ownership of it.
Watch the selected portions of Understanding Strings in Rust – String vs &str by Federico Terzi. Use the video to connect slices with the choice of function signature.
Understanding Strings in Rust - String vs &str
Understanding Strings in Rust – String vs &str shows when text can remain a borrowed view and when a function creates owned text.
Watch borrowed views for the contrast between trimming existing text and producing changed text. Then watch text ownership for literals versus dynamically created strings. Finish with function signatures. Treat the video's advice to return String as a useful pattern, not a rule: a function can return &str when its result is a view into an input.
The diagram makes the two possible sources of a &str visible. An owned String manages its heap buffer; a string slice can borrow bytes from that buffer or refer to literal text stored with the program.

The important consequence for a function is that receiving &str does not tell it where the bytes are stored. It tells the function something more relevant: “You may read this valid UTF-8 text while the borrow is available.”
Choose &str when the function only needs to read
Consider a function that trims whitespace from an input:
fn trimmed_view(text: &str) -> &str {
text.trim()
}
trim finds a view of the existing text. It does not need to construct another string, and neither does trimmed_view. Both its argument and its result are borrowed. The returned view remains tied to the input: the caller cannot keep using it after the underlying text has gone away.
The same signature accepts several useful forms of text:
fn main() {
let owned = String::from(" pending ");
let whole = trimmed_view(&owned);
let literal = trimmed_view(" ready ");
let portion = trimmed_view(&owned[2..9]);
println!("{whole}"); // pending
println!("{literal}"); // ready
println!("{portion}"); // pending
println!("{owned}"); // The caller still owns the String.
}
&owned initially borrows the String. At this function call, Rust can convert that borrow to the required &str; this is called deref coercion. The conversion does not copy the text or allocate a new buffer. If you prefer to make the view explicit, owned.as_str() also produces a &str.
The official Rust book makes this interface choice using a first_word function. You encountered slices in the previous lesson; this time, concentrate on why changing the parameter from &String to &str matters.
The Slice Type - The Rust Programming Language
Read the “String Slices as Parameters” subsection of The Slice Type in The Rust Programming Language to see how one signature accepts owned-string borrows, partial slices, and literals.
Under “String Slices as Parameters,” start where the book proposes the improved first_word signature. Read its example calls, then the parameter rationale. Stop before “Other Slices.” Focus on what the caller can pass, rather than revisiting how first_word finds a space.
This is a useful default for text inspection in an API application. If a function needs to check a supplied status, inspect a field, or format information from a field without retaining ownership of that field, &str keeps the input contract appropriately narrow.
Choose String when ownership must cross the boundary
Borrowing cannot do every job. A function may need to create independent text and give ownership of it to the caller:
fn make_label(status: &str) -> String {
format!("status: {status}")
}
The input is borrowed because make_label only reads it. The output is a String because "status: " plus the supplied text is new, independently owned text. The caller can keep that result even after the input it was made from is gone.
There is another reason to put String in a signature: the function needs to take ownership of an existing string. For example:
fn append_queue_marker(mut label: String) -> String {
label.push_str(" [queued]");
label
}
This function receives ownership of label, modifies it, and returns ownership of the resulting String. It may be able to use spare capacity in the existing buffer, although appending can still require a reallocation. Its caller gives up the original binding:
let label = String::from("status: pending");
let queued = append_queue_marker(label);
println!("{queued}");
// println!("{label}"); // Does not compile: label was moved.
Taking String should therefore express an actual requirement, not be a reflex whenever the caller happens to have one. If a function merely reads label, a String parameter imposes an unnecessary move. A caller that wants to retain its value would then have to clone it or redesign the call.
The standard library documentation states the corresponding default: functions generally accept &str unless they specifically need a String.
Read the “Deref” and opening “Representation” passages in Rust’s standard library documentation for String. They connect the inexpensive &String-to-&str conversion with what a String owns.
In “Deref,” find the paragraph immediately after the first code example and read from the interface guidance. Then, under “Representation,” read the opening description of the pointer, length, and capacity through the heap buffer. You can skip the later discussion of trait-related cases where automatic conversion needs help; it is not needed for ordinary function calls here.
A useful refinement is that “returns text” does not imply “returns String.” trimmed_view returns text borrowed from its input, so &str is right. make_label creates text that must have its own owner, so String is right. Conversely, a function taking String usually signals that ownership of the supplied value is part of its contract.
Why &String is usually too specific
Suppose the function only counts bytes:
fn byte_count(text: &String) -> usize {
text.len()
}
This compiles when given &owned_string, but it needlessly insists that the caller have a String. A string literal and a slice of a string are already &str; neither is a reference to a String. The more useful signature is:
fn byte_count(text: &str) -> usize {
text.len()
}
It performs the same operation and accepts all three: a literal, a borrowed String, or a partial slice. &String does not mean “more borrowed” than &str. Both borrow; &String additionally demands that the thing being borrowed be a String object.
There are legitimate exceptions. A function that inspects String-specific information, such as its reserved capacity, needs access to the String object:
fn reserved_bytes(text: &String) -> usize {
text.capacity() - text.len()
}
A &str has a length but does not own a buffer whose capacity it could report. And if a function must modify the caller’s existing String, it needs &mut String, not &String. If it must become the owner, it needs String. These are distinct contracts; choose the one that matches the job.
Read a complete set of signatures
The three common decisions fit together in one small program:
fn trimmed_view(text: &str) -> &str {
text.trim()
}
fn make_label(status: &str) -> String {
format!("status: {status}")
}
fn append_queue_marker(mut label: String) -> String {
label.push_str(" [queued]");
label
}
fn main() {
let source = String::from(" pending ");
let view = trimmed_view(&source);
let label = make_label(view);
println!("{source} | {view} | {label}");
println!("{}", trimmed_view(" ready "));
let queued = append_queue_marker(label);
println!("{queued}");
}
In order, trimmed_view borrows and returns a borrow; make_label borrows and returns new owned text; append_queue_marker takes ownership and returns ownership. The source remains usable because no function took ownership of it. The binding label, by contrast, is no longer usable after it is passed to append_queue_marker; its text is now represented by queued.
One boundary is worth making explicit: you cannot create a local String inside a function and return a &str pointing into it. That local owner would be dropped when the function returns. Return the String instead if the caller must keep newly created text.
Conclusion
Choose a text parameter by asking what the function requires: &str to read, &mut String to change the caller’s string, or String to take ownership. For results, return &str when the text remains a view into an input and String when the caller needs independent owned text. &String is rarely the best read-only parameter because it excludes literals and other slices without helping a function that only reads text.
Next, you’ll use these ownership and borrowing choices while storing and retrieving values in Vec and HashMap.