Архитектура приложения
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:
// 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 для управления состоянием:
// Пример 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 эндпоинты (БЕЗ обёртки)
Аутентификационные эндпоинты возвращают данные напрямую:
// POST /auth/sign-in
{ accessToken: string, refreshToken: string }
// GET /auth/me
{ id: string, email: string, firstName?: string, lastName?: string }Остальные эндпоинты (С обёрткой)
Все остальные эндпоинты возвращают данные в стандартной обёртке:
// GET /integrations
{ success: true, data: IIntegration[] }
// POST /integrations/:id/credentials
{ success: true, data: ICredential }API Service
Базовый сервис для работы с API. Автоматически извлекает data из обёртки:
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));
}
}Как это работает:
- API возвращает:
{ success: true, data: [...] } map(response => response.data)извлекаетdata- Компонент получает напрямую:
[...]
Interceptors
Auth Interceptor
Автоматически добавляет JWT токен ко всем запросам:
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:
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
// Logout logic
}
return throwError(() => error);
})
);
};Аутентификация
Flow
- Пользователь вводит credentials
- Отправка запроса на
/auth/login - Получение JWT токена
- Сохранение токена в localStorage
- Перенаправление в админ-панель
AuthService
@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");
}
}Структура сервисов
Все сервисы следуют единому паттерну:
@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):
@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:
// Импорт Material модулей в компоненте
imports: [
MatCardModule,
MatButtonModule,
MatIconModule,
MatTableModule,
MatDialogModule
// ...
];Темизация
Темы настраиваются в styles.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:
// 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 сборка
npm run build:prodГенерируются два варианта:
- dist/admin/browser — клиентская часть
- dist/admin/server — серверная часть для SSR
Docker
Проект включает Dockerfile для деплоя:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY dist ./dist
CMD ["node", "dist/admin/server/server.mjs"]Следующие шаги
- API Reference — изучите доступные сервисы
- Функции — узнайте о функциональности
- Правила кода — правила написания кода