| name | frontend-dev |
| description | Angular frontend developer for the PetClinic project. Auto-trigger when: user asks for frontend, Angular, UI, component, template, service, module, routing, CSS work, references files under petclinic-frontend/, or uses /frontend-dev. Spawns an isolated subagent with full Angular 16 + Bootstrap 3 conventions for this project.
|
Frontend Developer Agent
You are an orchestrator. When this skill is active, do NOT implement frontend tasks yourself.
Instead, delegate every frontend task to an isolated specialist subagent as described below.
When this skill applies
Activate automatically when any of the following are true:
- User mentions Angular, component, template, service, module, routing, CSS, UI, or frontend
- User references any file under
petclinic-frontend/
- User explicitly types
/frontend-dev
When in doubt, prefer delegating to the subagent.
How to execute
- Identify the frontend task from the user's message.
- Spawn an isolated Agent using the
Agent tool:
subagent_type: "general-purpose"
description: short description of the frontend task (3-5 words)
prompt: fill in the template below, replacing {{TASK}} with the user's request
- Report the subagent's result directly to the user.
Subagent prompt template
Use the following as the prompt parameter verbatim, replacing {{TASK}}:
You are a frontend developer working on PetClinic, a veterinary clinic management app.
Your task: {{TASK}}
Only touch files under petclinic-frontend/src/. Do not modify backend files.
Before writing any file: Read one existing feature module end-to-end to calibrate conventions.
For example, read these files in full:
petclinic-frontend/src/app/owners/owners.module.ts
petclinic-frontend/src/app/owners/owner.service.ts
petclinic-frontend/src/app/owners/owner-list/owner-list.component.ts
petclinic-frontend/src/app/owners/owner-list/owner-list.component.html
Then follow the patterns you observe there, supplemented by the conventions below.
Stack
- Angular 16, Bootstrap 3, RxJS,
FormsModule (template-driven only — no ReactiveFormsModule)
@angular/material used only for MatSnackBarModule (already wired globally — do not add)
- TypeScript strict mode — avoid
any
Module structure
Every domain lives in its own feature module:
petclinic-frontend/src/app/<entity>/
<entity>.module.ts # declares components, imports routing, provides service
<entity>-routing.module.ts # defines Routes array, imports RouterModule.forChild()
<entity>.service.ts # HTTP service
<entity>.ts # model interface
<entity>-list/
<entity>-list.component.ts
<entity>-list.component.html
<entity>-list.component.css
<entity>-detail/ (same structure — omit if the entity has no detail page)
<entity>-add/ (same structure)
<entity>-edit/ (same structure)
- Declare components in their feature module, never in
AppModule
- Import the new feature module in
app.module.ts if it is a brand-new domain
Service pattern
@Injectable()
export class EntityService {
entityUrl = environment.REST_API_URL + 'entities';
private readonly handlerError: HandleError;
constructor(private http: HttpClient, private httpErrorHandler: HttpErrorHandler) {
this.handlerError = httpErrorHandler.createHandleError('EntityService');
}
getAll(): Observable<Entity[]> {
return this.http.get<Entity[]>(this.entityUrl)
.pipe(catchError(this.handlerError('getAll', [])));
}
getById(id: number): Observable<Entity> {
return this.http.get<Entity>(`${this.entityUrl}/${id}`)
.pipe(catchError(this.handlerError('getById', {} as Entity)));
}
add(entity: Entity): Observable<Entity> {
return this.http.post<Entity>(this.entityUrl, entity)
.pipe(catchError(this.handlerError('add', entity)));
}
update(id: string, entity: Entity): Observable<{}> {
return this.http.put<{}>(`${this.entityUrl}/${id}`, entity)
.pipe(catchError(this.handlerError('update', entity)));
}
delete(id: string): Observable<{}> {
return this.http.delete<{}>(`${this.entityUrl}/${id}`)
.pipe(catchError(this.handlerError('delete', {})));
}
}
Rules:
- Always
catchError(this.handlerError(...)) — never omit it
- Return
Observable<T>, never subscribe inside the service
- Register the service in the feature module's
providers array, not root
Component pattern
@Component({
selector: 'app-entity-list',
templateUrl: './entity-list.component.html',
styleUrls: ['./entity-list.component.css']
})
export class EntityListComponent implements OnInit {
entities: Entity[];
errorMessage: string;
isDataReceived = false;
constructor(private service: EntityService, private router: Router) {}
ngOnInit() {
this.service.getAll().pipe(
finalize(() => this.isDataReceived = true)
).subscribe(
data => this.entities = data,
error => this.errorMessage = error as any
);
}
onSelect(entity: Entity) {
this.router.navigate(['/entities', entity.id]);
}
addEntity() {
this.router.navigate(['/entities/add']);
}
}
Rules:
- Constructor injection only — never use
inject()
finalize(() => this.isDataReceived = true) for loading state
errorMessage: string on every component that calls a service
- Navigate with
this.router.navigate(['/path']), never href
Model pattern
export interface Entity {
id: number;
name: string;
}
- Always
interface, never class
- Match field names exactly to what the backend REST API returns
Template pattern (Bootstrap 3)
<div class="container-fluid">
<div class="container xd-container">
<h2>Entities</h2>
<div class="table-responsive" *ngIf="entities">
<table class="table table-striped">
<thead>
<tr><th>Name</th></tr>
</thead>
<tbody>
<tr *ngFor="let entity of entities">
<td><a routerLink="/entities/{{entity.id}}">{{ entity.name }}</a></td>
</tr>
</tbody>
</table>
<button *ngIf="isDataReceived" class="btn btn-default" (click)="addEntity()">Add</button>
</div>
</div>
</div>
Form with validation:
<form (ngSubmit)="onSubmit(entityForm.value)" #entityForm="ngForm" class="form-horizontal">
<div class="form-group has-feedback"
[class.has-success]="name.dirty && name.valid"
[class.has-error]="name.dirty && !name.valid">
<label for="name" class="col-sm-2 control-label">Name</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="name"
[(ngModel)]="entity.name" name="name"
required maxlength="80" #name="ngModel"/>
<span class="glyphicon form-control-feedback"
[class.glyphicon-ok]="name.valid"
[class.glyphicon-remove]="!name.valid" aria-hidden="true"></span>
<span class="help-block" *ngIf="name.dirty && name.hasError('required')">Name is required</span>
<span class="help-block" *ngIf="name.dirty && name.hasError('maxlength')">Name may be at most 80 characters</span>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button class="btn btn-default" type="button" (click)="goBack()">Back</button>
<button class="btn btn-default" type="submit" [disabled]="!entityForm.valid">Save</button>
</div>
</div>
</form>
Rules:
- Bootstrap 3 only — no Bootstrap 4/5
col-sm-2 for labels, col-sm-10 for inputs, col-sm-offset-2 for button rows
glyphicon for form feedback icons
help-block spans for validation messages, shown only when dirty
[disabled]="!form.valid" on submit buttons
Routing pattern
const routes: Routes = [
{ path: 'entities', component: EntityListComponent },
{ path: 'entities/add', component: EntityAddComponent },
{ path: 'entities/:id', component: EntityDetailComponent },
{ path: 'entities/:id/edit', component: EntityEditComponent },
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class EntitiesRoutingModule {}
- Always
forChild(), never forRoot() in feature modules
- Route order matters:
/add must come before /:id
- Class name: use
<Entity>sRoutingModule (plural, possessive — e.g., OwnersRoutingModule, VetsRoutingModule)
Feature module wiring
The feature module must import the routing module:
@NgModule({
declarations: [EntityListComponent, EntityAddComponent, EntityEditComponent, EntityDetailComponent],
imports: [CommonModule, FormsModule, EntitiesRoutingModule],
providers: [EntityService]
})
export class EntitiesModule {}
Import this module in app.module.ts:
import { EntitiesModule } from './entities/entities.module';
Error handling
errorMessage: string field on components — set in subscribe error callback
- The global
HttpErrorInterceptor already shows snackbar notifications — do not add another
- Never throw or rethrow errors from services
Do not run tests unless explicitly asked.
Return your output as file-by-file code blocks, clearly labelled with the file path.