TL;DR: Can your Angular app write smarter? With Google Gemini API, it’s possible! In Part 1, we set up the project, secured the API key, built the AISuggestionService for real-time grammar checks, and added a signal-based Settings Component for key and token management. Next, we’ll bring this foundation to life with an interactive UI and advanced features in Part 2.
Writing great content is challenging, correcting grammar, improving flow, and staying creative takes time. Imagine an AI assistant that checks grammar in real time, suggests improvements, and boosts your writing confidence.
In this tutorial, we’ll show you how to integrate Google’s Gemini API into an Angular app to build your own AI-powered writing assistant.
The AI-powered writing assistant will provide:
By the end of Part 1, you’ll have a working AI Suggestion Service and Settings Component ready for production.
Let’s get started!
Before you start, make sure you have:
First, open your terminal and create a new Angular project:
ng new angular-gemini-writing-assistant --style=scss --zoneless=false This command creates a new Angular workspace and initial application with the following configurations:
To integrate the Google Gemini API into your Angular app, you’ll need an API key. Follow these steps:
Important: Each Gemini API key is linked to a Google Cloud project. If you don’t have a project yet, create one or import an existing project from Google Cloud into AI Studio.
Next, we’ll configure our Angular app to use this API key and start building the AI Suggestion Service.
The AISuggestionService is the core engine that powers your grammar assistant. It handles:
Since this service has multiple responsibilities, we’ll break it down into smaller chunks and explain each function in detail.
At the top of the src/app/services/ai-suggestion.ts file, define a JSON schema to enforce structured responses from Gemini.
const SUGGESTIONS_SCHEMA: JSONSchema = {
type: "object",
properties: {
suggestions: {
type: "array",
items: {
type: "object",
properties: {
text: { type: "string" },
originalText: { type: "string" },
},
required: ["text"],
},
},
},
required: ["suggestions"],
}; Why use a JSON schema?
The schema ensures a consistent structure, rather than relying on Gemini to return well-formatted JSON. This prevents complex parsing and ensures predictable, reliable responses.
Now, create the AISuggestionService class with all necessary dependencies as mentioned in the following code example.
export class AISuggestionService {
private readonly http = inject(HttpClient);
private readonly modelId = inject(MODEL_ID);
private readonly apiBase = inject(API_BASE);
private readonly timeoutMs = inject(TIMEOUT_MS);
private readonly maxOutputTokens = inject(MAX_OUTPUT_TOKENS);
private get apiUrl() {
const path = `models/${this.modelId}:generateContent`;
return `${this.apiBase}/${path}`;
}
private readonly defaultApiKey = "";
private apiKey = this.defaultApiKey;
private readonly tokenUsage = signal({
inputTokens: 0,
outputTokens: 0,
totalTokens: 0,
requestCount: 0,
});
getTokenUsage = this.tokenUsage.asReadonly();
private readonly aiStatus = signal({ kind: "ok" });
getAIStatus = this.aiStatus.asReadonly();
} This service uses Angular’s modern inject() function for dependency injection and signals for reactive state management. The readonly public signals (getTokenUsage, getAIStatus) ensure components can observe but not modify the service’s internal state.
The getSuggestions() is the primary method that orchestrates the entire grammar-checking workflow.
getSuggestions(text: string): Observable<AISuggestion[]> {
if (!this.apiKey || !this.apiKey.trim()) {
this.aiStatus.set({
kind: "noApiKey",
message: "No API key configured. Please add your Gemini API key in Settings.",
});
return of([
{
id: SUGGESTION_IDS.NO_API_KEY,
text: "No API key configured. Please go to Settings and enter your Google Gemini API key to enable grammar checking.",
},
]);
}
if (text.trim().length < 3) return of([]);
const prompt = this.buildGrammarPrompt(text);
const request: GeminiRequest = {
contents: [{ parts: [{ text: prompt }] }],
generationConfig: {
...AISuggestionService.defaultGenerationConfig(),
maxOutputTokens: this.maxOutputTokens,
responseMimeType: "application/json",
responseSchema: SUGGESTIONS_SCHEMA,
},
};
const headers = new HttpHeaders({ "Content-Type": "application/json" });
const params = new HttpParams().set("key", this.apiKey);
return this.http
.post<GeminiResponse>(this.apiUrl, request, { headers, params })
.pipe(
timeout(this.timeoutMs),
map((response) => {
this.aiStatus.set({ kind: "ok" });
return this.parseSuggestionsFromGemini(response);
}),
catchError((error) => this.handleApiError(error))
);
} What does this method do?
This orchestrates the entire grammar-checking workflow:
The buildGrammarPrompt() method crafts a carefully designed prompt that focuses Gemini’s attention strictly on grammar.
private buildGrammarPrompt(text: string): string {
return `You are an expert grammar checker. Analyze the following text and provide ONLY grammar suggestions for grammatical errors, spelling mistakes, and punctuation issues.
Text to analyze:
---
${text}
---
Return ONLY valid JSON (no code fences, no prose, no markdown).
Guidelines:
- Focus ONLY on grammar, spelling, and punctuation errors
- Do NOT provide clarity, style, or completion suggestions
- Provide 1-5 grammar corrections maximum
- If there are no grammar errors, return an empty suggestions array
- Include the exact original text that needs to be corrected in "originalText"`;
} Why is this prompt effective?
The parseSuggestionsFromGemini() method safely extracts and validates grammar suggestions from Gemini’s response.
private parseSuggestionsFromGemini(response: GeminiResponse): AISuggestion[] {
if (response.usageMetadata) {
this.tokenUsage.update((usage) => ({
inputTokens:
usage.inputTokens + (response.usageMetadata?.promptTokenCount ?? 0),
outputTokens:
usage.outputTokens + (response.usageMetadata?.candidatesTokenCount ?? 0),
totalTokens:
usage.totalTokens + (response.usageMetadata?.totalTokenCount ?? 0),
requestCount: usage.requestCount + 1,
}));
}
if (!response.candidates?.length) return [];
const rawText = response.candidates[0]?.content?.parts?.[0]?.text;
if (!rawText) return [];
let parsedData: unknown;
try {
parsedData = JSON.parse(rawText.trim());
} catch {
return [];
}
if (!this.isSuggestionsPayload(parsedData)) return [];
return this.mapItemsToSuggestions(parsedData.suggestions);
} This function returns empty arrays instead of throwing errors, uses optional chaining (?.) to safely navigate nested objects, wraps JSON parsing in try-catch, and validates structure with type guards before using the data.
Now let’s control Gemini’s response behavior:
private static defaultGenerationConfig() {
return {
temperature:0.3,
topK:40,
topP:0.95,
thinkingConfig:{thinkingBudget:0},
} as const;
} The parameters mentioned in the above code example control how the Gemini model should generate text:
Then, convert the raw API data to application format.
private mapItemsToSuggestions(items: unknown[]): AISuggestion[] {
const maxItems = 5;
const picked = [] as AISuggestion[];
for (let i = 0; i < items.length && picked.length < maxItems; i++) {
const item = items[i];
if (!this.isParsedSuggestion(item)) continue;
picked.push({
id: `gemini-${picked.length + 1}`,
text: item.text,
originalText: typeof item.originalText === "string" ? item.originalText : undefined,
});
}
return picked;
} Why this matters:
The Settings component provides a user interface for managing the Gemini API key and monitoring token usage. This component uses Angular’s modern input/output signals for parent-child communication and demonstrates several important patterns.
Let’s break down this component function by function to understand how each one works.
Let’s create the Settings component and update the src/app/components/settings/settings.ts file as shown in the following code example.
export class SettingsComponent implements OnInit {
private readonly aiSuggestionService = inject(AISuggestionService);
readonly settings = input.required<UserSettings>();
readonly tokenUsage = input.required<TokenUsage>();
readonly settingsChange = output<Partial<UserSettings>>();
readonly close = output<void>();
protected apiKeyValue = '';
ngOnInit() {
this.apiKeyValue = this.settings().geminiApiKey || '';
}
closeModal() {
this.close.emit();
}
} Key concepts:
Let’s perform API key management with the following code.
saveApiKey(apiKey: string) {
const trimmedKey = apiKey.trim();
if (!this.isValidApiKey(trimmedKey)) {
return;
}
this.updateApiKey(trimmedKey);
this.closeModal();
}
private isValidApiKey(apiKey: string | undefined): apiKey is string {
return !!apiKey && apiKey.length > 0;
}
private updateApiKey(apiKey: string | undefined): void {
this.settingsChange.emit({ geminiApiKey: apiKey });
this.aiSuggestionService.setApiKey(apiKey || '');
}
clearApiKey() {
this.apiKeyValue = '';
this.updateApiKey(undefined);
} How it works:
Now, update the src/app/components/settings/settings.html file as per the source code from GitHub.
The Settings component highlights several modern Angular features and best practices. It uses:
[(ngModel)] to keep the input field synchronized with the component property,keydown.enter adds support for saving input using the Enter key.@if ensures that usage statistics are displayed only when relevant data is available,Overall, the Settings component demonstrates clean and efficient Angular patterns, including signal-based inputs and outputs, type guards, and a clear separation between presentation and business logic.
Thanks for reading! In this first part of our series, we laid the foundation for an AI-powered writing assistant in Angular by:
With these pieces in place, your app now has a solid backend and configuration layer ready for real-time AI interactions.
You can also contact us through our support portal for queries. We are always happy to assist you!