With RockPlayerApi complete since article 6 (recommendation, cache and the YouTube adapter already working end to end), what's missing is the part the person actually sees: the screen. This article builds RockPlayerWeb, starting with the first thing any new person finds, the onboarding.
The folder structure, designed to grow without pain
No NgModule here, every component is standalone. The organization follows a simple division by responsibility:
src/app/
pages/ one folder per screen (onboarding, player)
models/ the types that flow through the application (PlaybackSource...)
services/ HTTP calls to RockPlayerApi
layout/ visual shell (the header)
None of this requires a monorepo or Nx to exist. The advantage of organizing it this way from the start is a different one: if one day the project grows to the point where it's worth splitting into multiple front-ends, moving a whole folder from pages/ to a new project is much simpler than untangling mixed files.
The onboarding: where everything starts
The first screen asks you to pick your favorite bands, exactly the cold start explained in article 1 of RockRecommender. For that, it needs a list of bands to show, and this is where a new piece appears: a GET /bands.
This endpoint exposes the catalog of bands that RockRecommender already keeps internally, and RockPlayerApi just forwards the call, following the same rule established since article 5: Angular never talks directly to RockRecommender, only to RockPlayerApi.
public interface IRecommenderClient
{
Task<Result<Guid>> CreateUserAsync(IEnumerable<string> likedBands);
Task<Result<NextSong>> GetNextSongAsync(Guid userId);
Task SubmitFeedbackAsync(Guid userId, Guid songId, bool liked);
// new since article 5
Task<Result<List<string>>> GetBandsAsync();
}
public sealed class BandService(IRecommenderClient recommenderClient)
{
public Task<Result<List<string>>> GetBandsAsync() => recommenderClient.GetBandsAsync();
}
[Route("bands")]
public sealed class BandsController(BandService bandService) : ApiControllerBase
{
[HttpGet]
public async Task<ActionResult<List<string>>> GetBands()
{
var result = await bandService.GetBandsAsync();
return ToActionResult(result);
}
}
IRecommenderClient gains one more method since article 5, and the usual rule still applies: the controller only knows BandService, never IRecommenderClient directly.
Fetching the list of bands in Angular
A simple service, using HttpClient (which under the hood already uses Fetch, as we saw in article 4). API_BASE_URL is just a one-line constant holding RockPlayerApi's address, shared by every service that needs to call it:
@Injectable({ providedIn: 'root' })
export class BandsService {
private readonly http = inject(HttpClient);
getBands() {
return this.http.get<string[]>(API_BASE_URL + '/bands');
}
}
The onboarding, built with signals
The band selection is kept in a simple signal, checked and unchecked on every click:
@Component({
selector: 'app-onboarding',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './onboarding.component.html',
})
export class OnboardingComponent {
private readonly bandsService = inject(BandsService);
private readonly usersService = inject(UsersService);
private readonly router = inject(Router);
readonly bands = toSignal(this.bandsService.getBands(), { initialValue: [] as string[] });
readonly selectedBands = signal<string[]>([]);
toggleBand(band: string) {
this.selectedBands.update((current) =>
current.includes(band) ? current.filter((existing) => existing !== band) : [...current, band]);
}
submit() {
this.usersService.createUser(this.selectedBands()).subscribe((userId) => {
this.router.navigate(['/player', userId]);
});
}
}
No Subject, no ngOnInit to fetch data on entry, toSignal already takes care of that. And since OnPush is the default starting with Angular 22, this screen doesn't even need to declare anything extra to react correctly when bands arrives from the API.
What's missing
With the person registered and redirected, only the screen that actually plays the song, receives like and dislike and asks for the next recommendation is left. That is article 8, the last one in the series.