Back to Blog
AI & DevelopmentMay 14, 202610 min read

Prompt Engineering for Angular Code Quality

DR

Doug Ribeiro

10 min read

After a long stretch of iterating on how I use AI assistants in my Angular workflow, I've come to a conclusion: the quality of the code you get back is almost entirely determined by the quality of the prompt you write. That sounds obvious, but most developers treat AI like a senior colleague who already knows their codebase, their conventions, and their constraints. It doesn't. You have to teach it every single time. And the way you teach it is through the prompt.

None of what follows is the generic 'be specific, give context' advice. These are the prompts I actually reach for when I generate components, services, tests and architecture decisions in Angular, shown before and after, because the difference between a vague prompt and a precise one is easier to see than to describe.

Context SettingTell the model the stack, constraints, and goal 1 Specific InstructionsBe explicit: TypeScript, Angular, no any, use signals 2 Example-DrivenProvide input/output examples or existing patterns 3 Iterative RefinementReview output, correct mistakes, re-prompt with fixes 4

Why Angular Needs Different Prompts

Angular is opinionated. It has a specific way of doing dependency injection, a module system (or standalone components if you're on v15+, which hit developer preview in 14.0), RxJS conventions, strong TypeScript expectations, and a very particular file structure. Generic AI models know Angular, but they don't know which Angular. They might generate code using NgModules when you're on a standalone project, or use class-based components with lifecycle hooks when you've adopted signals. You need to tell them.

On top of that, enterprise Angular projects have their own layers of convention: shared libraries, design system components, specific state management patterns, naming conventions for services vs facades vs resolvers. None of that is in the model's training data. So the burden is on you to provide it.

The System Prompt / Context Block I Always Start With

Whether I'm using a chat-based tool or an IDE integration, the first thing I do is establish a context block. I either set this as a system prompt or paste it at the top of every conversation. Here's roughly what it looks like for a standalone, signals-based project. The version line and the constraints change from codebase to codebase:

text
You are an Angular expert assisting a senior developer on an enterprise Angular 17.3+ project.

Constraints:
- Use standalone components only. No NgModules.
- Use Angular Signals for local state. Avoid BehaviorSubject for component-level state.
- RxJS is acceptable for HTTP streams and complex async flows.
- All components must be typed. No use of 'any'.
- Use inject() function for dependency injection, not constructor injection.
- Follow Angular style guide naming: MyFeatureComponent, MyFeatureService, etc.
- Use OnPush change detection strategy on all components.
- Tests use Jest + Angular Testing Library. No TestBed unless strictly necessary.
- CSS is SCSS. No inline styles.
- All observables must be unsubscribed. Prefer async pipe or takeUntilDestroyed().

When generating code, always provide the full file, not snippets.

This block alone changes the output dramatically. Before I added it, I'd get constructor-injected services, NgModule boilerplate, and components without OnPush. After, I get something much closer to what I'd actually write.

Prompting for Components

Before: The Vague Request

Early on, I'd write something like this:

text
Create an Angular component for a user profile card.

What I'd get back was a class-based component with constructor injection, no types on inputs, no OnPush, and sometimes an NgModule declaration. Technically correct for Angular 2018, not useful for my project.

After: The Structured Request

text
Create a standalone Angular 17.3+ component called UserProfileCardComponent.

Inputs:
- user: input.required<User>() (interface: { id: string; name: string; email: string; avatarUrl: string | null })
- isLoading: input<boolean>(false)

Behavior:
- When isLoading is true, show a skeleton placeholder (use a div with class 'skeleton')
- When user.avatarUrl is null, show initials from user.name in a circle div
- Emit an output called profileClicked: OutputEmitterRef<string> emitting user.id on card click

Style: SCSS. Card has a shadow and rounded corners. Use BEM naming.
Change detection: OnPush.
Do not use @Input() decorator. Use the input() signal API.
Provide the full .ts and .html files.

This produces a component I can drop into the project with maybe one or two tweaks. The key differences: I specified the Angular version, the API style (signal inputs), the exact interface shape, the behavior logic, and the output format. The version has to be precise. Signal inputs landed in 17.1 and output() with OutputEmitterRef in 17.3, so a model told plain 'Angular 17' is entitled to hand you @Input() and EventEmitter and be correct about it.

Prompting for Services

Services need a different template because the primary concerns are different: injection scope, HTTP handling, error management, and return types.

text
Create an Angular service called UserService.

- Injectable at root level
- Uses inject(HttpClient) for HTTP calls
- Base URL comes from inject(APP_CONFIG) token, property: apiBaseUrl
- Methods:
  1. getUser(id: string): Observable<User>
     - GET /users/:id
     - Pipe through catchError, rethrow as typed AppError
  2. updateUser(id: string, payload: Partial<User>): Observable<User>
     - PATCH /users/:id
     - Same error handling
- AppError interface: { code: string; message: string; statusCode: number }
- No BehaviorSubject or state management inside this service. Pure data access layer.
- Use typed HttpClient (pass response type in generic).

The critical things I always include for services: the injection scope, where config comes from, the exact method signatures with return types, and what the service should NOT do. That last point is surprisingly important. If you don't say 'this is a pure data layer', the model will often add a BehaviorSubject cache and suddenly your service is doing state management.

Prompting for Tests

Test generation is where prompt engineering pays off the most. The gap between a generic test and a useful test is enormous, and it's almost entirely controllable through the prompt.

Bad Test Prompt

text
Write tests for UserProfileCardComponent.

You'll get shallow render tests that check if the component renders, maybe one or two property bindings. Nothing that would actually catch a regression.

Good Test Prompt

text
Write Jest tests for UserProfileCardComponent using Angular Testing Library (render from @testing-library/angular).

Test these scenarios:
1. When isLoading is true, the skeleton placeholder is visible and the user name is not
2. When isLoading is false and user.avatarUrl is null, initials are derived from user.name and shown
3. When isLoading is false and user.avatarUrl is set, an img tag is rendered with the correct src
4. Clicking the card emits the correct user.id through the profileClicked output
5. Component renders without errors with minimum required input (user with all fields, isLoading false)

Do not use TestBed directly. Use the render() function from Testing Library.
Mock no services. This component has no service dependencies.
Use userEvent from @testing-library/user-event for interaction tests.

The result is a test file with actual coverage of real behavior. I define the scenarios explicitly because the model doesn't know what matters in my business context. Only I do.

Prompting for Architecture Decisions

Beyond code generation, I also use AI for architecture questions. Those need a very different kind of prompt, one that carries enough context for the model to reason about tradeoffs instead of picking the obvious answer. The example below is a template. The domain in it is deliberately plain, because what does the work is the constraint list and the last line, not the subject matter.

text
I'm building a self-contained feature module in Angular 17.3+ (standalone).

Context:
- The screen renders 6 independent panels, each with its own data source
- Panels can be shown or hidden per user preference (stored through a preferences API)
- Data refreshes every 30 seconds or on user request
- The app uses NgRx for global state but this feature should stay self-contained

Question: Should I use:
a) NgRx feature state for this module
b) A signal-based store (a plain service with signals)
c) NgRx ComponentStore

Explain the tradeoffs for each in this specific context. Consider DevTools debugging, boilerplate cost, testability, and the refresh pattern. Don't just list general pros/cons. Apply them to my situation.

The last sentence is crucial. Without it, you get a Wikipedia-style list of tradeoffs that doesn't help you decide anything. Asking the model to apply the reasoning to your specific situation forces it to take a position.

What to Always Include in Context

  • Angular version and major feature flags (standalone, signals, etc.)
  • Which state management pattern you're using and at what scope
  • The testing stack (Jest vs Karma, Testing Library vs TestBed)
  • Your CSS approach (SCSS, Tailwind, CSS Modules, design system tokens)
  • Specific APIs that are off-limits or required (inject() vs constructor, input() vs @Input)
  • File output format expectations (full file vs snippet)
  • Any interfaces or types the generated code needs to conform to

Iterating on Prompts Like Code

I keep a personal file of prompt templates I've refined over time. Basically a prompts.md in my notes. When a prompt produces great output, I save it. When it fails, I note why and adjust. This is the same feedback loop I use for refining code. Prompt engineering is a craft, and you get better at it through deliberate iteration.

One pattern I've found consistently useful: when a generated output is wrong, don't just retry with a slightly different prompt. Instead, analyze exactly what was wrong. Was it the Angular version assumed? The injection pattern? The missing interface? Then fix that specific gap in the prompt. Fuzzing the prompt randomly wastes time.

The Limits of Prompt Engineering

I want to be honest about what this approach doesn't solve. No amount of prompt engineering replaces understanding what you're asking for. If I don't know what OnPush change detection means, I can't specify it correctly and I won't notice when the generated code uses Default instead. The prompt amplifies your knowledge. It doesn't substitute for it.

The other limit is anything that spans multiple files at once. A feature made of a component, a service, a resolver and a store is still hard to generate in one shot. I break those into smaller, well-prompted pieces and assemble them manually. That's not a failure of the tool; it's just an accurate understanding of what it's good at.

Precision Beats Cleverness

The developers I see getting the most out of AI tools aren't the ones with the most creative prompts. They're the ones who are the most precise. They know their stack well enough to specify it accurately. They define interfaces before asking for implementations. They tell the model what not to do as clearly as they tell it what to do.

If you take one thing from this post: treat your context block as infrastructure. Keep it versioned, keep it updated as your stack evolves, and share it with your team. A good system prompt is a team asset.

References

AIAngularPrompt EngineeringTypeScript