Generate Angular Service
Create a new Angular service named $ARGUMENTS.
Requirements
Read the project's
CLAUDE.mdfor conventions, folder structure, and state management approach.Determine the state management pattern:
- If
--ngrxis provided or the project uses NgRx Store, generate NgRx artifacts (actions, reducer, selectors, effects). - Otherwise, generate a signal-based service.
- If
Determine the target directory:
Standard Angular CLI project:
- If the service is app-wide (auth, API, config), place under
src/app/core/services/. - If the service is feature-specific, place under
src/app/features/<feature>/services/. - If unclear, ask.
Nx monorepo project:
- If the service is a data-access concern (API calls, state), place under
libs/ui/data-access/src/lib/.- API services go in
api/. - NgRx state goes in
state/<feature>/(actions, reducers, selectors, effects). - HTTP/WebSocket clients go in
clients/.
- API services go in
- If the service is a shared utility (navigation, theme, dialogs), place under
libs/ui/shared-ui/src/services/. - If unclear, ask.
- If the service is app-wide (auth, API, config), place under
Generate the appropriate files (see patterns below).
Pattern A: Signal-Based Service
Generate these files:
<name>.service.ts- Service class<name>.service.spec.ts- Unit tests
typescript
@Injectable({ providedIn: 'root' })
export class NameService {
private readonly http = inject(HttpClient);
// Private writable signals for state
private readonly _items = signal<Item[]>([]);
private readonly _loading = signal(false);
private readonly _error = signal<string | null>(null);
// Public readonly selectors
readonly items = this._items.asReadonly();
readonly loading = this._loading.asReadonly();
readonly error = this._error.asReadonly();
readonly itemCount = computed(() => this._items().length);
loadItems(): void {
this._loading.set(true);
this._error.set(null);
this.http.get<Item[]>('/api/items').pipe(
tap(items => {
this._items.set(items);
this._loading.set(false);
}),
catchError(err => {
this._error.set(err.message);
this._loading.set(false);
return EMPTY;
})
).subscribe();
}
}Signal-Based Service Test
typescript
describe('NameService', () => {
let service: NameService;
let httpTesting: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
provideHttpClient(),
provideHttpClientTesting()
]
});
service = TestBed.inject(NameService);
httpTesting = TestBed.inject(HttpTestingController);
});
afterEach(() => httpTesting.verify());
it('should load items', () => {
const mockItems = [{ id: '1', name: 'Test' }];
service.loadItems();
expect(service.loading()).toBe(true);
const req = httpTesting.expectOne('/api/items');
req.flush(mockItems);
expect(service.items()).toEqual(mockItems);
expect(service.loading()).toBe(false);
expect(service.error()).toBeNull();
});
it('should handle error', () => {
service.loadItems();
const req = httpTesting.expectOne('/api/items');
req.error(new ProgressEvent('error'), { status: 500, statusText: 'Server Error' });
expect(service.items()).toEqual([]);
expect(service.loading()).toBe(false);
expect(service.error()).toBeTruthy();
});
});Pattern B: NgRx Store
Generate these files in state/<feature-name>/:
<name>.actions.ts- Action definitions<name>.reducers.ts- Reducer and state interface<name>.selectors.ts- Memoized selectors<name>.effects.ts- Side effects<name>.reducers.spec.ts- Reducer tests<name>.selectors.spec.ts- Selector tests<name>.effects.spec.ts- Effect tests
Also generate in api/:
<name>-api.service.ts- HTTP API service (no state, just HTTP calls)<name>-api.service.spec.ts- API service tests
Actions
typescript
import { createActionGroup, emptyProps, props } from '@ngrx/store';
export const ItemsActions = createActionGroup({
source: 'Items',
events: {
'Load Items': emptyProps(),
'Load Items Success': props<{ items: Item[] }>(),
'Load Items Failure': props<{ error: string }>(),
'Add Item': props<{ item: Omit<Item, 'id'> }>(),
'Add Item Success': props<{ item: Item }>(),
'Add Item Failure': props<{ error: string }>(),
}
});Reducer
typescript
import { createReducer, on } from '@ngrx/store';
import { EntityAdapter, EntityState, createEntityAdapter } from '@ngrx/entity';
import { ItemsActions } from './items.actions';
export interface ItemsState extends EntityState<Item> {
loading: boolean;
error: string | null;
}
export const itemsAdapter: EntityAdapter<Item> = createEntityAdapter<Item>();
const initialState: ItemsState = itemsAdapter.getInitialState({
loading: false,
error: null
});
export const itemsReducer = createReducer(
initialState,
on(ItemsActions.loadItems, state => ({
...state,
loading: true,
error: null
})),
on(ItemsActions.loadItemsSuccess, (state, { items }) =>
itemsAdapter.setAll(items, { ...state, loading: false })
),
on(ItemsActions.loadItemsFailure, (state, { error }) => ({
...state,
loading: false,
error
})),
on(ItemsActions.addItemSuccess, (state, { item }) =>
itemsAdapter.addOne(item, state)
)
);Selectors
typescript
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { ItemsState, itemsAdapter } from './items.reducers';
const selectItemsState = createFeatureSelector<ItemsState>('items');
const { selectAll, selectEntities, selectTotal } = itemsAdapter.getSelectors();
export const ItemsSelectors = {
selectAllItems: createSelector(selectItemsState, selectAll),
selectItemEntities: createSelector(selectItemsState, selectEntities),
selectItemCount: createSelector(selectItemsState, selectTotal),
selectLoading: createSelector(selectItemsState, state => state.loading),
selectError: createSelector(selectItemsState, state => state.error),
};Effects
typescript
import { inject, Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { catchError, exhaustMap, map, of } from 'rxjs';
import { ItemsActions } from './items.actions';
import { ItemsApiService } from '../../api/items-api.service';
import { NotificationsActions } from '../notifications/notifications.actions';
@Injectable()
export class ItemsEffects {
private readonly actions$ = inject(Actions);
private readonly api = inject(ItemsApiService);
readonly loadItems$ = createEffect(() =>
this.actions$.pipe(
ofType(ItemsActions.loadItems),
exhaustMap(() =>
this.api.getItems().pipe(
map(items => ItemsActions.loadItemsSuccess({ items })),
catchError(error => of(
ItemsActions.loadItemsFailure({ error: error.message }),
NotificationsActions.showError({ message: 'Failed to load items' })
))
)
)
)
);
}API Service (used by effects)
typescript
@Injectable({ providedIn: 'root' })
export class ItemsApiService {
private readonly http = inject(HttpClient);
getItems(): Observable<Item[]> {
return this.http.get<Item[]>('/api/items');
}
createItem(item: Omit<Item, 'id'>): Observable<Item> {
return this.http.post<Item>('/api/items', item);
}
}NgRx Registration
Register the new state slice in the data-access configuration:
typescript
// data-access-state.config.ts
export function provideDataAccessState() {
return [
provideState('items', itemsReducer),
provideEffects(ItemsEffects),
// ... other state slices
];
}NgRx Tests
Reducer test:
typescript
describe('ItemsReducer', () => {
it('should set loading on loadItems', () => {
const state = itemsReducer(initialState, ItemsActions.loadItems());
expect(state.loading).toBe(true);
expect(state.error).toBeNull();
});
it('should set items on loadItemsSuccess', () => {
const items = [{ id: '1', name: 'Test' }];
const state = itemsReducer(initialState, ItemsActions.loadItemsSuccess({ items }));
expect(selectAll(state)).toEqual(items);
expect(state.loading).toBe(false);
});
});Selector test:
typescript
describe('ItemsSelectors', () => {
it('should select all items', () => {
const state = { items: itemsAdapter.setAll([{ id: '1', name: 'Test' }], initialState) };
expect(ItemsSelectors.selectAllItems.projector(state.items)).toEqual([{ id: '1', name: 'Test' }]);
});
});Effect test:
typescript
describe('ItemsEffects', () => {
let effects: ItemsEffects;
let actions$: Observable<Action>;
let api: jest.Mocked<ItemsApiService>;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
ItemsEffects,
provideMockActions(() => actions$),
{ provide: ItemsApiService, useValue: { getItems: jest.fn() } }
]
});
effects = TestBed.inject(ItemsEffects);
api = TestBed.inject(ItemsApiService) as jest.Mocked<ItemsApiService>;
});
it('should load items on success', () => {
const items = [{ id: '1', name: 'Test' }];
api.getItems.mockReturnValue(of(items));
actions$ = of(ItemsActions.loadItems());
effects.loadItems$.subscribe(action => {
expect(action).toEqual(ItemsActions.loadItemsSuccess({ items }));
});
});
});General Conventions
- Use
inject()function (not constructor injection) - Use
providedIn: 'root'for app-wide services; omit and provide in routes for feature-scoped - Use
unknowninstead ofanyfor ambiguous types - Define interfaces for API response types in a
models/ortypes/folder - Handle loading and error state explicitly
Output
After generating, summarize:
- Files created and their locations
- State management pattern used (signal-based or NgRx)
- Public API (signals/selectors, methods/actions)
- API endpoints used
- Any model interfaces created