Production-oriented Angular admin starter
Angular 22 · Material · Transloco · JWT refresh · RBAC · PWA
- What is Dragon?
- What makes it different
- Architecture
- Quick start
- Configuration
- Authentication & session
- RBAC
- Building screens
- HTTP client & interceptor
- List / filters / pagination
- Forms, dialogs & unsaved changes
- i18n & RTL
- Theme
- PWA
- Scripts & tests
- Contributing
- License
Dragon is an open-source Angular 22 admin boilerplate: standalone components, Angular Material, lazy routes, JWT login with refresh, resource permissions, Transloco i18n (including RTL), shared CRUD tables, and a production PWA.
Clone it, point apiUrl at your admin API, add pages; keep auth, HTTP, theme, and table patterns.
Keywords: angular admin boilerplate, angular material dashboard, jwt refresh, rbac, transloco rtl, pwa starter
Most Angular starters stop at “login + sidebar”. Dragon is opinionated about admin day-to-day:
| Area | Typical starter | Dragon |
|---|---|---|
| Auth | Access JWT only | Access + refresh; interceptor retries once |
| Session | Token in memory | localStorage session; JWT expiry checked before the request |
| Login | Hard redirect to / |
?returnUrl= after silent logout / deep link |
| Permissions | Role name string | Resource RBAC (city:read, user:write, …) |
| Write vs read | Separate checks | :write also satisfies matching :read |
| Lists | Client search= query |
Filter DSL + paging + sort |
| CRUD | Copy-paste per page | Shared createListPage() (load, empty, error, dialog, delete) |
| Forms | Close dialog anytime | Unsaved-form guard on create / edit |
| i18n | English only | en / ar / ckb, RTL, paginator + snackbars translated |
| Errors | Console only | 403 / 404 / 500 pages, global handler, API field errors |
| Offline | None | Production service worker; API traffic is never cached |
Browser
→ Guards (guest / auth / permission / first-tab / unsaved-form)
→ Pages (standalone, lazy loadComponent)
→ HttpService + apiInterceptor
→ /api/admin/v1 (proxied in dev)
src/app/
@core/ auth, HTTP, interceptor, guards, models, i18n, theme, spinner, PWA
@shared/ logo, dialogs, list-page helper, permission directive
dragon/
auth/ login
home/ shell (toolbar + sidenav)
dashboard/ landing page
setting/ system, activities, cache, regions, cities
account/ roles, users, login blocks, customers
accounting/ currencies, account credits
section/ shared tab shell for admin sections
src/environments/ environment.ts, environment.prod.ts
src/assets/i18n/ en.json, ar.json, ckb.json
src/assets/img/ logo.png, logo-sm.png
public/ PWA manifest and icons
| Route prefix | Section |
|---|---|
/login |
Sign in (guest only) |
/dashboard |
Home |
/settings |
System, activities, cache, regions, cities |
/accounts |
Roles, users, login blocks, customers |
/accounting |
Currencies, account credits |
/403 /404 /500 |
Error pages |
Dev proxy: proxy_local.json forwards /api/** to the backend.
- Node.js 22.22.3+ (24 and 26 also supported) — see
.nvmrc - npm 10+
- An admin API on the proxy target (default
http://0.0.0.0:6969)
nvm use
npm install
npm startOpen http://localhost:4200.
npm run build
npx ng build --configuration productionProduction output: dist/dragon/browser.
Edit src/environments/environment.ts (and environment.prod.ts for production).
| Key | Purpose |
|---|---|
apiUrl |
Admin API prefix, default /api/admin/v1 |
localStorageKey |
Session key (operator-dragon) |
theme |
Color palette (see Theme) |
defaultLang |
en, ar, or ckb |
perPageOptions |
Table page sizes |
snackBarDuration |
Toast duration in ms |
Change the backend host/port in proxy_local.json. Restart npm start after env or proxy edits.
| File | Used for |
|---|---|
src/assets/img/logo.png |
Favicon, README, PWA-scale mark |
src/assets/img/logo-sm.png |
Toolbar and login (<app-logo>) |
public/icons/*.png |
Installable PWA icons |
Keep logo-sm.png square; the chrome uses it at 34–72px.
POST /api/admin/v1/login
{ "username": "admin", "password": "…" }Session is stored in localStorage (token, refresh_token, resources, …).
Authorization: Bearer <access_token>
POST /api/admin/v1/refresh-token
{ "refresh_token": "<refresh_token>" }- Interceptor attaches
Authorizationexcept on login and refresh - Expired access or
401→ refresh once, then retry - You stay logged in while access or refresh JWT is valid
- Failed refresh →
/login?returnUrl=… authGuardprotects the shell;guestGuardkeeps signed-in users off/login- Mutating requests show the spinner and a success toast on
200
Empty resources is full access.
Routes use permissionGuard (data.permission). Missing access opens /403; a section opens on the first tab you can use. Buttons use *appPermission="'city:write'". Write also satisfies the matching read.
Catalog: src/app/@core/nav/permissions.ts. Add a resource there, on the route/tab, and on write actions.
- Endpoint —
ApiEndPointsinsrc/app/@core/const.ts - Model —
src/app/@core/models/models.ts - List page — copy Regions; point
createListPageat the endpoint, search columns, and dialog - Dialog — reactive form,
UnsavedFormService,ErrorHandlingComponent - Route — lazy
loadComponent+permissionGuard(andunsavedFormGuardif the page opens dialogs) - Tab —
src/app/@core/nav/admin-nav.ts - i18n —
en.json/ar.json/ckb.json - Permission —
*appPermission="'resource:write'"on add / edit / delete
readonly list = createListPage<Region>({
endpoint: ApiEndPoints.REGIONS,
searchColumns: ['name'],
dialog: RegionDialogComponent,
});createListPage() owns paging, search, sort, dialogs, delete, loading / error, and unsubscribes when the page is destroyed.
this.http.post(ApiEndPoints.REGIONS, this.form.value).subscribe({
next: () => this.dialogRef.close(true),
});Use put(endpoint, body, id) for edits and delete(endpoint, id) for removes. HttpService unwraps { data: … }.
HttpService (src/app/@core/services/http.service.ts):
| Method | Maps to |
|---|---|
list |
GET /:url?page&page_size&order_by&direction&filter |
get |
GET /:url → response.data |
getByID |
GET /:url/:id |
post |
POST /:url |
put |
PUT /:url/:id |
delete |
DELETE /:url/:id |
Do not call HttpClient from pages.
Interceptor (src/app/@core/interceptor/http.interceptor.ts):
- Skips i18n JSON under
assets/i18n - Refresh + retry on expired access or
401(once) - Maps
invalid_paramsonto form fields viaValidatorService - Toasts API errors;
401logs out;403stays on the page and toasts
Tables send a filter query, not search:
filter=name[like]"%erb%"
filter=full_name[like]"%erb%"[or]username[like]"%erb%"[or]phone[like]"%erb%"
| Screen | Columns |
|---|---|
| Users / Customers | full_name, username, phone |
| Roles | name |
| Regions / Cities / Currencies | name |
| Settings | property, value, description |
| Activities | event, username, ip, uri |
| Account credits | currency |
| Login blocks | key |
A numeric query also adds id[eq]"…". Operators: [eq] [ne] [gt] [lt] [gte] [lte] [like] [and] [or].
Helper: likeOrFilter(columns, term) in src/app/@shared/crud/crud.util.ts.
Default list params: page=1, page_size from perPageOptions[0], order_by=id, direction=desc.
- Dialogs use
dialogSize()(maxWidth/maxHeightfor mobile,disableClose: true) UnsavedFormServicetracks dirty forms;unsavedFormGuardasks before leave- Delete uses
confirmDelete()→AlertMessageComponent - Field errors from the API (
invalid_params) render throughErrorHandlingComponent
Do not bypass the unsaved guard by calling dialogRef.close() on a dirty form without unsaved.tryDiscard().
Transloco. Files: src/assets/i18n/{en,ar,ckb}.json.
| Code | Language | Direction |
|---|---|---|
en |
English | LTR |
ar |
Arabic | RTL |
ckb |
Central Kurdish | RTL |
Language switch lives on login and in the header. Choice is stored as dragon-lang. index.html sets lang / dir before bootstrap to avoid a flash.
When you add a string, add it to all three files.
Paginator labels go through AppPaginatorIntl. Snackbars follow RTL (start / end).
Set theme in the environment to one of:
indigo · violet · teal · rose · amber · forest · ocean
theme: 'forest',Restart after changing the palette. Users still toggle dark / light from the header; that choice is dragon-color-scheme in localStorage (or prefers-color-scheme on first visit). Palettes live in src/app/@core/theme/app-themes.ts and src/app.scss.
Production builds register @angular/service-worker (ngsw-config.json). The app shell, icons, i18n, and fonts are cached; /api/** is not. npm start does not register the worker.
Install from the browser on HTTPS or localhost. After a new deploy, a snackbar offers Reload.
Manifest and icons: public/.
| Command | Description |
|---|---|
npm start |
Dev server with API proxy |
npm run build |
Default build |
npm test |
Karma unit tests |
npm run test:ci |
Headless, one run |
npm run lint |
ESLint |
npm run format |
Prettier rewrite of src/ |
npm run format:check |
Prettier check |
npm run e2e |
Playwright |
npm run e2e:ui |
Playwright UI |
npx playwright install chromium # first time only
npm run e2ePlaywright starts the dev server and mocks the API.
Stars help others find this boilerplate. Issues and PRs welcome.
Topics: angular angular-material admin-dashboard boilerplate jwt rbac pwa transloco playwright
MIT — see LICENCE.
Author: Barez Azad
