DEV Community

Umberto Antonio Cicero
Umberto Antonio Cicero

Posted on

How I Structure Angular + Docker + AI Projects After 14 Years of Engineering

How I Structure Angular + Docker + AI Projects

Every time I start a new Angular project, I used to waste 2-3 hours on the same boring setup:

  • Folder structure? Let me think again...
  • Docker? Copy from the last project and fix the broken parts...
  • AI integration? Search Stack Overflow for 30 minutes...
  • CI/CD? Another hour of YAML debugging...

After 14 years building software across oil & gas, critical infrastructure, and intelligent transport systems, I finally created the setup I wish I had on day one.

Here's exactly how I structure it — and why.


🏗️ The Architecture: Core / Shared / Features

src/app/
├── core/           → Singleton services, interceptors (loaded ONCE)
├── shared/         → Reusable components (used everywhere)
├── features/       → Lazy-loaded pages (loaded on demand)
├── app.config.ts   → Standalone bootstrap
└── app.routes.ts   → Route definitions
Enter fullscreen mode Exit fullscreen mode

Why this pattern?

  • core/ = things that exist once (HTTP interceptors, auth, AI service)
  • shared/ = things used in multiple features (header, sidebar, buttons)
  • features/ = each page is independent and lazy-loaded

This is not my opinion — this is the pattern that scales from a side project to a 50-developer team. I've used it in airports, highways, and oil rigs.


🤖 AI Integration in 30 Lines

The key insight: don't overthink it. You need one service.

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

  chat(messages: ChatMessage[], apiKey: string): Observable<string> {
    const headers = new HttpHeaders({
      'Content-Type': 'application/json',
      Authorization: `Bearer ${apiKey}`,
    });

    return this.http
      .post<ChatCompletionResponse>(
        'https://api.openai.com/v1/chat/completions',
        {
          model: 'gpt-4o-mini',
          messages,
          max_tokens: 2048,
        },
        { headers }
      )
      .pipe(map((res) => res.choices[0]?.message?.content ?? ''));
  }
}
Enter fullscreen mode Exit fullscreen mode

Then in any component:

this.ai.chat([
  { role: 'user', content: 'Explain Docker in 3 sentences' }
], apiKey).subscribe(response => console.log(response));
Enter fullscreen mode Exit fullscreen mode

That's it. No complex abstractions. No wrapper libraries. Just Angular's HttpClient talking to OpenAI.

⚠️ In production, route calls through your backend. Never expose API keys in the frontend.


🐳 Docker: Multi-Stage Build

# Stage 1: Build
FROM node:20-alpine AS build
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci
COPY . .
RUN npm run build:prod

# Stage 2: Serve
FROM nginx:alpine AS production
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist/app/browser /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Enter fullscreen mode Exit fullscreen mode

Result: ~25MB production image. Fast to deploy, tiny to store.

I pair this with a docker-compose.yml that gives you both dev and prod:

# Production
docker-compose up --build

# Development (with hot reload)
docker-compose --profile dev up app-dev
Enter fullscreen mode Exit fullscreen mode

⚡ The Details That Matter

Path Aliases (no more ../../../)

// tsconfig.json
"paths": {
  "@core/*": ["src/app/core/*"],
  "@shared/*": ["src/app/shared/*"],
  "@features/*": ["src/app/features/*"]
}
Enter fullscreen mode Exit fullscreen mode

Now your imports look like:

import { AiService } from '@core/services/ai.service';
Enter fullscreen mode Exit fullscreen mode

Instead of:

import { AiService } from '../../../core/services/ai.service';
Enter fullscreen mode Exit fullscreen mode

Signals for UI State

export class LoadingService {
  private requestCount = signal(0);
  readonly isLoading = computed(() => this.requestCount() > 0);
}
Enter fullscreen mode Exit fullscreen mode

Angular Signals > RxJS BehaviorSubjects for simple UI state. Less boilerplate, better performance.

HTTP Interceptors (Functional)

export const errorInterceptor: HttpInterceptorFn = (req, next) => {
  return next(req).pipe(
    catchError((error: HttpErrorResponse) => {
      // Handle 401, 429, 500, etc.
      notification.error(getErrorMessage(error));
      return throwError(() => error);
    })
  );
};
Enter fullscreen mode Exit fullscreen mode

No classes. No implements HttpInterceptor. Just a function. This is Angular 17+.


📁 The Full Stack

Layer Tech
Framework Angular 17+ (standalone, signals, new control flow)
Containerization Docker multi-stage + Docker Compose
AI OpenAI API (gpt-4o-mini)
Web Server Nginx (gzip, caching, security headers)
CI/CD GitHub Actions
Styling SCSS + CSS custom properties (dark/light theme)

🎯 TL;DR

After 14 years and dozens of projects across enterprise, infrastructure, and industrial domains, this is the setup I always come back to:

  1. Core/Shared/Features architecture with lazy loading
  2. Standalone components (no NgModules)
  3. Signals for UI state
  4. One clean AI service — no over-abstraction
  5. Multi-stage Docker build (~25MB image)
  6. Path aliases for clean imports
  7. Functional interceptors for HTTP

I packaged all of this into a ready-to-use starter kit. If you want to skip the 2-3 hours of setup and start building immediately:

👉 Angular + Docker + AI Starter Kit on Gumroad

Clone. Run. Ship.


What's your go-to Angular project structure? Let me know in the comments!

Top comments (0)