Random Quote Generator
Create an Angular quote app with categories, favorites, and one-click sharing
Time to implement the project: ~ 8-12 hours
- Angular
- Angular Routing
- HTTP Client
- RxJS Basics
- State Handling
- Angular Material
- Local Storage
In this beginner Angular project, you will build a Random Quote Generator that fetches quotes from an external API and renders them in a clean, readable UI. The app must display the quote text, author (if available), and a category selector that filters which type of quote is requested. Users should be able to generate a new quote on demand and see immediate feedback during loading and error states.
You will add a favorites list that saves selected quotes locally, allowing users to revisit them across sessions. The app must also include a share button that copies a shareable quote message to the clipboard and supports a direct “share” flow where the browser provides it. Build the UI with Angular Material to keep components accessible, consistent, and fast to assemble in Angular.
What This Project Teaches
This project is designed to strengthen core Angular skills that show up in real applications: calling APIs with the Angular HTTP client, handling responses through observables, and updating the UI based on state. You will practice building a small feature set end-to-end: category selection, data fetching, conditional rendering, and user actions that update local persistence.
The goal is not to build a complex system. The goal is to build a dependable Angular app with clear structure, predictable UI behavior, and an interface that feels complete.
Prerequisites and Setup Requirements
You should already know the Angular basics and feel comfortable building components and binding data in templates. This project expects that you can run an Angular app locally and inspect requests in DevTools.
- Angular fundamentals: components, templates, and data binding
- Basic understanding of services and dependency injection
- Familiarity with Angular HttpClient for API requests
- Basic RxJS concepts (subscribe, map) and async UI states
- Comfort using Angular Material components and layout utilities
- Basic localStorage usage for saving favorites
Beginner-Friendly Requirements That Still Feel Professional
A good Random Quote Generator feels smooth and intentional: categories filter correctly, the UI communicates loading and errors, favorites remain stable, and sharing works in a predictable way. These requirements focus on Angular structure and user experience rather than heavy engineering.
| Requirement | Explanation |
| Fetch quotes via Angular HttpClient | Using Angular’s built-in HTTP layer reinforces correct patterns for API-driven apps. |
| Category selector and filtering | Filtering teaches state-driven UI updates and keeps the app from feeling like a single-button demo. |
| Loading, empty, and error states | Clear states build user trust and show professional attention to real network behavior. |
| Favorites list with persistence | Saving favorites demonstrates local state storage and makes the project useful beyond one session. |
| Share button behavior | Sharing validates integration with browser APIs and reinforces practical product interactions. |
| Accessible UI built with Angular Material | Material components provide consistent interactions and accessible defaults for beginner projects. |
| Clean component/service separation | Separating fetching logic from UI keeps your Angular code maintainable and review-ready. |
Tips for a Clean Angular Implementation
Build the API layer first: create a quote service that returns observables and hides endpoint details from components. Then build a simple component that renders one quote, and expand it to support categories and favorites. Keep UI state explicit: loading, error, and success should be represented by clear variables so templates stay readable. Use Angular Material for inputs, buttons, snackbars, and cards to keep the UI consistent without custom styling overhead. When your template reads like a state machine, debugging becomes straightforward.
- Use a dedicated service for API calls and keep components focused on rendering and events
- Persist favorites in localStorage with a stable shape to avoid broken restores
- Prevent duplicate favorites by checking quote IDs or a normalized text+author key
- Use a snackbar or toast after copy/share so users get immediate confirmation
- Handle missing authors gracefully to keep UI consistent across quote sources
- Keep category options explicit to avoid unpredictable API results
Common Mistakes When Building a Random Quote Generator
1. Fetching quotes directly inside the component instead of using a service
A Random Quote Generator is a small project, so it is tempting to put everything inside one component: API URL, HTTP request, loading state, error handling, and template logic. This works for a quick demo, but it teaches the wrong Angular habit. As soon as you add categories, favorites, sharing, or another quote source, the component becomes difficult to read and reuse.
Problematic approach:
export class QuoteComponent {
quote: any;
loading = false;
error = "";
constructor(private http: HttpClient) {}
getQuote(): void {
this.loading = true;
this.http.get("https://api.example.com/random").subscribe({
next: (response) => {
this.quote = response;
this.loading = false;
},
error: () => {
this.error = "Could not load quote.";
this.loading = false;
}
});
}
}
The component now knows too much about the API. If the endpoint changes, the response shape changes, or you add category support, the component becomes responsible for details that should belong to a service.
Better approach:
export interface Quote {
id: string;
text: string;
author: string;
category: string;
}
@Injectable({
providedIn: "root"
})
export class QuoteService {
private apiUrl = "https://api.example.com/quotes";
constructor(private http: HttpClient) {}
getRandomQuote(category: string): Observable<Quote> {
return this.http
.get<QuoteResponse>(`${this.apiUrl}/random`, {
params: { category }
})
.pipe(
map((response) => ({
id: response.id || `${response.text}-${response.author}`,
text: response.text,
author: response.author || "Unknown author",
category
}))
);
}
}
Cleaner component usage:
export class QuoteComponent {
quote: Quote | null = null;
loading = false;
error = "";
constructor(private quoteService: QuoteService) {}
loadQuote(category: string): void {
this.loading = true;
this.error = "";
this.quoteService.getRandomQuote(category).subscribe({
next: (quote) => {
this.quote = quote;
this.loading = false;
},
error: () => {
this.error = "Quote could not be loaded. Try again.";
this.loading = false;
}
});
}
}
Pay attention to: Keep API logic inside a dedicated Angular service. Components should focus on user actions and rendering, while services should handle endpoints, response mapping, and reusable data access.
2. Ignoring loading, error, and empty states
Many beginners build only the happy path: click a button, receive a quote, show the quote. But real API requests can be slow, fail, return incomplete data, or return nothing. If the UI does not show what is happening, users may click the button repeatedly or think the app is broken.
Problematic template:
<mat-card>
<p>{{ quote.text }}</p>
<span>{{ quote.author }}</span>
</mat-card>
<button mat-raised-button (click)="loadQuote()">
New quote
</button>
This template assumes that quote always exists. Before the first request finishes, the app may show errors in the console or render broken content.
Better template:
<mat-card *ngIf="loading">
<mat-progress-spinner mode="indeterminate"></mat-progress-spinner>
<p>Loading a new quote...</p>
</mat-card>
<mat-card *ngIf="error && !loading" class="error-card">
<p>{{ error }}</p>
<button mat-button type="button" (click)="loadQuote(selectedCategory)">
Try again
</button>
</mat-card>
<mat-card *ngIf="quote && !loading && !error">
<blockquote>{{ quote.text }}</blockquote>
<p>— {{ quote.author || "Unknown author" }}</p>
</mat-card>
<button
mat-raised-button
color="primary"
type="button"
[disabled]="loading"
(click)="loadQuote(selectedCategory)"
>
{{ loading ? "Loading..." : "New quote" }}
</button>
Pay attention to: A beginner project feels much more professional when it communicates state clearly. Add loading feedback, retry behavior, missing-author handling, and a disabled button while a request is already running.
3. Saving duplicate favorites because quotes do not have a stable key
Favorites are usually where a simple quote generator starts to become useful. A common mistake is pushing every selected quote into an array without checking whether it already exists. Some APIs provide quote IDs, while others return only text and author. If you do not create a stable key, the same quote can be saved many times.
Problematic code:
favorites: Quote[] = [];
addToFavorites(quote: Quote): void {
this.favorites.push(quote);
localStorage.setItem("favoriteQuotes", JSON.stringify(this.favorites));
}
This version does not prevent duplicates. It also does not separate favorite management from the quote display component.
Better approach:
function createQuoteKey(quote: Quote): string {
return `${quote.text.trim().toLowerCase()}::${quote.author.trim().toLowerCase()}`;
}
@Injectable({
providedIn: "root"
})
export class FavoritesService {
private storageKey = "favoriteQuotes";
getFavorites(): Quote[] {
const saved = localStorage.getItem(this.storageKey);
if (!saved) {
return [];
}
try {
return JSON.parse(saved);
} catch {
return [];
}
}
addFavorite(quote: Quote): Quote[] {
const favorites = this.getFavorites();
const quoteKey = createQuoteKey(quote);
const alreadySaved = favorites.some((favorite) => {
return createQuoteKey(favorite) === quoteKey;
});
if (alreadySaved) {
return favorites;
}
const updatedFavorites = [...favorites, quote];
localStorage.setItem(this.storageKey, JSON.stringify(updatedFavorites));
return updatedFavorites;
}
}
Component usage:
saveCurrentQuote(): void {
if (!this.quote) return;
this.favorites = this.favoritesService.addFavorite(this.quote);
this.snackBar.open("Quote saved to favorites.", "Close", {
duration: 2500
});
}
Pay attention to: Use a stable ID when the API provides one. If it does not, create a normalized key from quote text and author. This keeps the favorites list clean and predictable.
4. Reading localStorage without validation
localStorage is useful for a small Angular project, but it should not be treated as always safe and perfectly formatted. Users can clear storage, browser
extensions can modify it, and old versions of your app may leave outdated data behind. If your app blindly parses stored values, one broken value can crash the whole
favorites page.
Problematic code:
loadFavorites(): Quote[] {
return JSON.parse(localStorage.getItem("favoriteQuotes")!);
}
This code assumes the value exists and contains valid JSON. If the value is missing or corrupted, the app throws an error.
Better approach:
loadFavorites(): Quote[] {
const savedFavorites = localStorage.getItem("favoriteQuotes");
if (!savedFavorites) {
return [];
}
try {
const parsedFavorites = JSON.parse(savedFavorites);
if (!Array.isArray(parsedFavorites)) {
return [];
}
return parsedFavorites.filter((quote) => {
return (
typeof quote.text === "string" &&
typeof quote.author === "string"
);
});
} catch {
localStorage.removeItem("favoriteQuotes");
return [];
}
}
Safer save method:
saveFavorites(favorites: Quote[]): void {
const safeFavorites = favorites.map((quote) => ({
id: quote.id,
text: quote.text,
author: quote.author || "Unknown author",
category: quote.category || "general"
}));
localStorage.setItem("favoriteQuotes", JSON.stringify(safeFavorites));
}
Pay attention to: Treat localStorage as external data. Parse it carefully, validate the shape, provide a fallback, and avoid letting broken saved data crash the app.
5. Implementing sharing without browser fallbacks
A share button sounds simple, but browser support and user permissions can vary. Some browsers support the Web Share API, others only support clipboard access, and some environments may block clipboard writes. A weak implementation assumes one method always works and gives users no feedback when it fails.
Problematic code:
shareQuote(): void {
navigator.share({
text: `${this.quote.text} — ${this.quote.author}`
});
}
This can fail if navigator.share is unavailable or if the user cancels the action. It also does not handle an empty current quote.
Better approach:
async shareQuote(quote: Quote | null): Promise<void> {
if (!quote) {
this.snackBar.open("Generate a quote first.", "Close", {
duration: 2500
});
return;
}
const shareText = `"${quote.text}" — ${quote.author || "Unknown author"}`;
try {
if (navigator.share) {
await navigator.share({
title: "Quote",
text: shareText
});
return;
}
await navigator.clipboard.writeText(shareText);
this.snackBar.open("Quote copied to clipboard.", "Close", {
duration: 2500
});
} catch {
this.snackBar.open("Sharing was cancelled or failed.", "Close", {
duration: 2500
});
}
}
Template usage:
<button
mat-stroked-button
type="button"
[disabled]="!quote"
(click)="shareQuote(quote)"
>
Share quote
</button>
Pay attention to: Sharing should feel reliable. Check whether a browser API exists, provide a clipboard fallback, handle cancellation, and show a short confirmation message with a snackbar or toast.
By completing this project, you'll gain a solid understanding of how to build an Angular app that fetches data from an API, renders results through clear UI states, and adds practical product features such as categories, favorites persistence, and share actions. This foundation strengthens your Angular fundamentals and prepares you for larger apps that rely on services, routing, and scalable component structure.
Reference Implementations Worth Studying
Direct quote-generator reference:
draganbozhinoski - Angular Random Quote Generator
This is the most direct reference because it is specifically a random quotes generator built in Angular. The repository is small, focused, and useful for understanding the basic shape of this project: an Angular app that displays quote content and keeps the scope narrow enough for a beginner-friendly implementation.
Pay particular attention to:
- How the project keeps the feature set simple instead of becoming a large dashboard.
- How Angular project files are organized around a small single-purpose app.
- Where quote rendering logic lives and how it could be separated into smaller components.
- How you could extend the base idea with categories, favorites, loading states, and sharing.
- What parts of the implementation are enough for practice and what parts should be improved for a portfolio version.
Use this repository as a basic orientation point. It is helpful for seeing the minimal version of the idea, but your own project should go further by adding service-based API logic, local favorites, error states, and a more polished Angular Material interface.
Stronger HttpClient and RxJS reference:
alisaduncan - Tutorial Angular HttpClient
This repository is not a quote generator, but it is a better reference for the Angular data-fetching side of the project. It demonstrates Angular HttpClient usage, RxJS, Angular Material, API calls to JSONPlaceholder, POST behavior, HTTP interceptors, and unit-test examples. These are exactly the habits that make a small quote app feel more professional.
When studying the code, focus on:
- How API calls are separated from component presentation.
- How HttpClient responses are handled in a structured Angular project.
- How Angular Material can support a clean and consistent interface.
- How tests can be added around service behavior instead of testing only visual output.
- How error handling and interceptors could later support a more realistic API-driven app.
This implementation is useful if you want your Random Quote Generator to be more than a button that calls an endpoint. Study the service structure and data-flow patterns, then adapt them to quote categories, loading states, and retry behavior.
Alternative persistence and CRUD reference:
weblineindia - Angular Local Storage CRUD Management
This project is useful as an alternative reference because it focuses on Angular, Angular Material, localStorage, and CRUD-style data operations. It manages product details rather than quotes, but the persistence pattern can be adapted for saved favorites, removing favorites, editing custom quote notes, or building a small saved-quotes library.
While reviewing this project, examine:
- How Angular Material forms and inputs are used for a consistent UI.
- How localStorage is used to keep data available after page refresh.
- How CRUD actions are organized around a simple data model.
- How a product-management structure can be simplified into quote favorites management.
- How local persistence changes the app from a temporary API demo into something users can return to later.
Use this repository for the favorites part of your quote app. The domain is different, but the important lesson is the same: saved user data should have a stable shape, predictable update methods, and a UI that makes adding, removing, and reviewing items easy.