Skip to content

Архитектура приложения

Integ Admin построен на Angular 20 с использованием современных подходов и best practices.

Общая архитектура

Приложение использует модульную архитектуру с четким разделением ответственности:

src/app/
├── core/           # Ядро приложения
├── admin/          # Админ секция
└── shared/         # Shared компоненты

Core

Содержит основную функциональность приложения:

  • Services — бизнес-логика и работа с API
  • Guards — защита маршрутов
  • Interceptors — обработка HTTP запросов
  • Interfaces — TypeScript типы и интерфейсы
  • Routes — определение роутов

Admin

Содержит админскую часть приложения:

  • Layout — общий layout с навигацией
  • Pages — страницы функциональности (features)

Shared

Содержит переиспользуемые компоненты:

  • Components — UI компоненты (dialogs, editors и т.д.)

Роутинг

Приложение использует Angular Router с lazy loading:

typescript
// core/routes/core.routes.ts
export const CORE_ROUTES: Routes = [
  {
    path: 'auth',
    children: [
      { path: 'login', loadComponent: ... },
      { path: 'register', loadComponent: ... }
    ]
  },
  {
    path: 'admin',
    canActivate: [authGuard],
    loadComponent: ...,
    children: [
      { path: 'dashboard', loadComponent: ... },
      { path: 'integrations', children: [...] },
      { path: 'testing', loadChildren: ... },
      { path: 'emulator', loadComponent: ... },
      { path: 'prompts', loadChildren: ... },
      { path: 'd1', children: [...] },
      { path: 'kv', children: [...] },
      { path: 'inngest', loadComponent: ... },
      { path: 'settings', children: [
        { path: 'account', loadComponent: ... },
        { path: 'access-tokens', loadComponent: ... }
      ]}
    ]
  }
];

Защита маршрутов

Используются два guard'а:

  • authGuard — проверяет авторизацию для доступа к админ-панели
  • noAuthGuard — редирект для авторизованных пользователей (login/register)

State Management

Приложение использует @ngneat/elf для управления состоянием:

typescript
// Пример store
import { createStore } from "@ngneat/elf";
import { withEntities } from "@ngneat/elf-entities";

const integrationsStore = createStore({ name: "integrations" }, withEntities<IIntegration>());

Преимущества Elf

  • Легковесный (меньше boilerplate, чем NgRx)
  • Типобезопасный
  • Поддержка DevTools
  • Персистентность состояния

HTTP Layer

Формат API Ответов

API использует два разных формата для разных типов эндпоинтов:

Auth эндпоинты (БЕЗ обёртки)

Аутентификационные эндпоинты возвращают данные напрямую:

typescript
// POST /auth/sign-in
{ accessToken: string, refreshToken: string }

// GET /auth/me
{ id: string, email: string, firstName?: string, lastName?: string }

Остальные эндпоинты (С обёрткой)

Все остальные эндпоинты возвращают данные в стандартной обёртке:

typescript
// GET /integrations
{ success: true, data: IIntegration[] }

// POST /integrations/:id/credentials
{ success: true, data: ICredential }

API Service

Базовый сервис для работы с API. Автоматически извлекает data из обёртки:

typescript
interface IApiWrapper<T> {
	success: boolean;
	data: T;
}

@Injectable({ providedIn: "root" })
export class ApiService {
	private readonly http = inject(HttpClient);

	// Все методы автоматически обрабатывают обёрнутый ответ
	get<T>(url: string) {
		return this.http.get<IApiWrapper<T>>(url).pipe(map((response) => response.data));
	}

	post<T>(url: string, data: unknown) {
		return this.http.post<IApiWrapper<T>>(url, data).pipe(map((response) => response.data));
	}

	put<T>(url: string, data: unknown) {
		return this.http.put<IApiWrapper<T>>(url, data).pipe(map((response) => response.data));
	}

	patch<T>(url: string, data: unknown) {
		return this.http.patch<IApiWrapper<T>>(url, data).pipe(map((response) => response.data));
	}

	delete<T>(url: string) {
		return this.http.delete<IApiWrapper<T>>(url).pipe(map((response) => response.data));
	}
}

Как это работает:

  1. API возвращает: { success: true, data: [...] }
  2. map(response => response.data) извлекает data
  3. Компонент получает напрямую: [...]

Interceptors

Auth Interceptor

Автоматически добавляет JWT токен ко всем запросам:

typescript
export const authInterceptor: HttpInterceptorFn = (req, next) => {
	const token = localStorage.getItem("access_token");

	if (token) {
		req = req.clone({
			setHeaders: { Authorization: `Bearer ${token}` }
		});
	}

	return next(req);
};

Error Interceptor

Обрабатывает ошибки и выполняет logout при 401:

typescript
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
	return next(req).pipe(
		catchError((error: HttpErrorResponse) => {
			if (error.status === 401) {
				// Logout logic
			}
			return throwError(() => error);
		})
	);
};

Аутентификация

Flow

  1. Пользователь вводит credentials
  2. Отправка запроса на /auth/login
  3. Получение JWT токена
  4. Сохранение токена в localStorage
  5. Перенаправление в админ-панель

AuthService

typescript
@Injectable({ providedIn: "root" })
export class AuthService {
	private readonly api = inject(ApiService);

	login(credentials: ILoginRequest) {
		return this.api.post<IAuthResponse>("/auth/login", credentials).pipe(
			tap((response) => {
				localStorage.setItem("access_token", response.accessToken);
			})
		);
	}

	logout() {
		localStorage.removeItem("access_token");
		this.router.navigate(["/auth/login"]);
	}

	isAuthenticated() {
		return !!localStorage.getItem("access_token");
	}
}

Структура сервисов

Все сервисы следуют единому паттерну:

typescript
@Injectable({ providedIn: "root" })
export class IntegrationsService {
	private readonly api = inject(ApiService);

	getAll() {
		return this.api.get<IIntegration[]>("/integrations");
	}

	getById(id: string) {
		return this.api.get<IIntegration>(`/integrations/${id}`);
	}

	create(data: ICreateIntegrationRequest) {
		return this.api.post<IIntegration>("/integrations", data);
	}

	update(id: string, data: IUpdateIntegrationRequest) {
		return this.api.put<IIntegration>(`/integrations/${id}`, data);
	}

	delete(id: string) {
		return this.api.delete<null>(`/integrations/${id}`);
	}
}

Компоненты

Все компоненты являются standalone (без NgModules):

typescript
@Component({
	selector: "app-integration-detail",
	standalone: true,
	imports: [
		CommonModule,
		MatCardModule,
		MatButtonModule
		// ...
	],
	templateUrl: "./integration-detail.component.html",
	styleUrl: "./integration-detail.component.scss"
})
export class IntegrationDetailComponent {
	private readonly route = inject(ActivatedRoute);
	private readonly service = inject(IntegrationsService);

	// Component logic
}

Преимущества standalone

  • Меньше boilerplate
  • Лучшая tree-shaking
  • Более явные зависимости
  • Рекомендуемый подход в Angular 20

Angular Material

Приложение использует Angular Material 20 с Material Design 3:

typescript
// Импорт Material модулей в компоненте
imports: [
	MatCardModule,
	MatButtonModule,
	MatIconModule,
	MatTableModule,
	MatDialogModule
	// ...
];

Темизация

Темы настраиваются в styles.scss:

scss
@use "@angular/material" as mat;

$theme: mat.define-theme(
	(
		color: (
			theme-type: light,
			primary: mat.$violet-palette
		)
	)
);

html {
	@include mat.all-component-themes($theme);
}

Server-Side Rendering

Приложение поддерживает SSR для улучшенного SEO:

typescript
// server.ts
export function app() {
	const server = express();
	const serverDistFolder = dirname(fileURLToPath(import.meta.url));
	const browserDistFolder = resolve(serverDistFolder, "../browser");

	// Angular Universal rendering
	server.get("*.*", express.static(browserDistFolder));
	server.get("*", (req, res) => {
		// SSR logic
	});

	return server;
}

Принципы кодирования

Проект следует строгим правилам написания кода:

  • Минимализм — код должен быть лаконичным
  • Early return — избегаем вложенных if
  • Типобезопасность — все типизировано
  • DI через inject() — используем functional injection API
  • Standalone components — без NgModules

Deployment

Production сборка

bash
npm run build:prod

Генерируются два варианта:

  • dist/admin/browser — клиентская часть
  • dist/admin/server — серверная часть для SSR

Docker

Проект включает Dockerfile для деплоя:

dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY dist ./dist
CMD ["node", "dist/admin/server/server.mjs"]

Следующие шаги

SaaS Admin Documentation