Монорепозиторий для самых маленьких: как разделить бэкенд и фронтенд для удобного управления проектом

Салимжанов Р.

Разделение бэкенда и фронтенда — ключевой шаг для создания масштабируемых и поддерживаемых приложений. Но как организовать их так, чтобы не запутаться в десятках файлов и сервисов? В этой статье вы узнаете, как использовать монорепозиторий и Docker для изоляции компонентов, упрощения разработки и быстрого развёртывания.

Зачем разделять бэкенд и фронтенд?

Изоляция кода: Бэкенд (логика) и фронтенд (интерфейс) работают независимо.

Масштабируемость: Легко добавлять новые сервисы или обновлять существующие.

Упрощение CI/CD: Каждый компонент можно тестировать и деплоить отдельно.

Но чтобы это работало, важно правильно организовать структуру проекта. Здесь на помощь приходит монорепозиторий. Кстати сразу хочу оговорится, это статья скажем так путеводитель для начинающих. Здесь содержится идея, а не готовый продукт для всего и вся, поэтому помидорами не кидайтесь.

Структура монорепозитория

Монорепозиторий — это единое хранилище для всех частей проекта. Вот как может выглядеть структура для двух приложений (app1 и app2):

mono-repo/ ├── apps/ │ ├── app1/ │ │ ├── frontend/ # React, Vue, Angular │ │ └── backend/ # Java, Node.js, Python │ └── app2/ │ ├── frontend/ │ └── backend/ ├── packages/ # Общие компоненты └── docker-compose.yml # Управление контейнерами

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

1)Все компоненты в одном месте.

2)Общие библиотеки (например, UI-компоненты) можно использовать в нескольких приложениях.

3)Упрощённая настройка зависимостей.

Настраиваем тестовой бэкенд и фронтенд

Для примера создадим два приложения (app1 и app2), где бэкенд будет возвращать строку, а фронтенд — отображать её. Код будет одинаковым для обоих приложений — разница только в названиях и строках.

1. Бэкенд (Java Spring Boot)

Для app1

Файл: apps/app1/backend/demo/src/main/java/com/example/demo1/HelloController.java

import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { @GetMapping("/api") public String getMessage() { return "Привет это бекенд от 1"; } }

Для app2

Файл: apps/app2/backend/demo/src/main/java/com/example/demo2/HelloController.java

import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { @GetMapping("/api") public String getMessage() { return "Hello from Java backend!"; } }

Что здесь происходит?

Каждый бэкенд запускается на своём порту(server.port=8082 - для app1 server.port=8081 - для app2).

По пути /api возвращается уникальная строка для каждого приложения.

А да точно еще следует сделать WebConfig для портов, а то будет возникать ошибка.

import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins("http://localhost:3002") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS"); } }

2. Фронтенд (React)

Для app1

Файл: apps/app1/frontend/demofront1/src/App.tsx

import React, { useEffect, useState } from 'react'; import '@mantine/core/styles.css'; import { MantineProvider, Container, Title, Text } from '@mantine/core'; function App() { const [message, setMessage] = useState(''); useEffect(() => { fetch('/api') .then(res => res.text()) .then(data => setMessage(data)); }, []); return ( <MantineProvider> <Container> <Title order={1} mt="md"> app2test Frontend </Title> <Text size="lg" mt="sm"> Backend response: {message} </Text> </Container> </MantineProvider> ); }

Для app2

Файл: apps/app2/frontend/demofront/src/App.tsx

import React, { useEffect, useState } from 'react'; function App() { const [message, setMessage] = useState(''); useEffect(() => { fetch('/api') .then(res => res.text()) .then(data => setMessage(data)); }, []); return ( <div> <h1>app1 Frontend</h1> <p>{message}</p> </div> ); } export default App;

Что здесь происходит?

Фронтенд отправляет запрос к /api и отображает ответ бэкенда.

Настраиваем Docker

1. Dockerfile для бэкенда

Для каждого приложения создайте файл Dockerfile в папке бэкенда:

Монорепозиторий для самых маленьких: как разделить бэкенд и фронтенд для удобного управления проектом
# Используем базовый образ с Maven FROM maven:3.8-openjdk-17 AS build # Устанавливаем рабочую директорию WORKDIR /app # Копируем pom.xml и устанавливаем зависимости COPY pom.xml . RUN mvn dependency:go-offline # Копируем исходный код COPY src ./src # Собираем проект RUN mvn package -DskipTests # Используем базовый образ с Java для запуска FROM openjdk:17 # Устанавливаем рабочую директорию WORKDIR /app # Копируем собранный JAR-файл COPY --from=build /app/target/*.jar app.jar # Открываем порт 8082 EXPOSE 8082 # Для app1 используйте 8082, для app2 — 8081 # Команда для запуска приложения CMD ["java", "-jar", "app.jar"]

2. Dockerfile для фронтенда

# Используем базовый образ Node.js для сборки FROM node:18 AS build # Устанавливаем рабочую директорию WORKDIR /app # Копируем package.json и package-lock.json COPY package*.json ./ # Устанавливаем зависимости RUN npm install # Копируем исходный код COPY . . # Собираем проект RUN npm run build # Используем базовый образ Nginx для запуска FROM nginx:alpine # Копируем собранные файлы фронтенда COPY --from=build /app/build /usr/share/nginx/html # Копируем конфигурацию Nginx COPY nginx.conf /etc/nginx/conf.d/default.conf # Открываем порт 80 EXPOSE 80 # Команда для запуска Nginx CMD ["nginx", "-g", "daemon off;"]

3. Настройка Nginx (nginx.conf)

Для каждого фронтенда создайте файл nginx.conf:

server { listen 80; server_name localhost; location / { root /usr/share/nginx/html; try_files $uri $uri/ /index.html; } # Проксирование API-запросов к бэкенду location /api { proxy_pass http://app1test-backend:8082; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } # Для app2: # location /api { # proxy_pass http://app2-backend:8081; # proxy_set_header Host $host; # proxy_set_header X-Real-IP $remote_addr; # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # proxy_set_header X-Forwarded-Proto $scheme; # } }

Связываем всё через docker-compose.yml

Чтобы связать все компоненты вашего приложения с помощью Docker Compose, вам нужно создать файл docker-compose.yml. Этот файл обычно размещается в mono-repo/.

Файл: docker-compose.yml

services: app2test-backend: build: ./apps/app2test/backend/demo ports: - "8081:8081" networks: - app-network app2test-frontend: build: ./apps/app2test/frontend/demofront ports: - "3000:80" depends_on: - app2test-backend networks: - app-network app1test-backend: build: ./apps/app1test/backend/demo1 ports: - "8082:8082" networks: - app-network app1test-frontend: build: ./apps/app1test/frontend/demofront1 ports: - "3002:80" depends_on: - app1test-backend networks: - app-network networks: app-network: driver: bridge

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

Каждое приложение работает в своей «паре» (бэкенд + фронтенд).

Сервисы общаются через общую сеть app-network.

Фронтенд app1 доступен на http://localhost:3002 , app2 — на http://localhost:3000.

Запускаем проект

Перейдите в корневую папку (mono-repo).

Выполните:

docker-compose up --build

Проверьте:

app1: http://localhost:3002 → Должно отобразиться:

Монорепозиторий для самых маленьких: как разделить бэкенд и фронтенд для удобного управления проектом

app2: http://localhost:3000 → Должно отобразиться:

Монорепозиторий для самых маленьких: как разделить бэкенд и фронтенд для удобного управления проектом

Как это влияет на безопасность

  1. Изоляция контейнеров Каждый сервис работает в своей «песочнице». Даже если злоумышленник взломает фронтенд, он не сможет напрямую получить доступ к данным бэкенда.

  2. Минимальные права Nginx во фронтенде имеет доступ только к статическим файлам, а бэкенд не имеет доступа к файловой системе фронтенда.

  3. Сетевые ограничения Сервисы общаются через внутреннюю сеть Docker, которая недоступна из внешнего мира. Порт бэкенда (8081/8082) открыт только для фронтенда.

  4. Лёгкое обновление Если в одном из компонентов обнаружена уязвимость, можно быстро пересобрать только его контейнер, не затрагивая остальные части системы.

Итог

Используя монорепозиторий и Docker, вы:

  • Упрощаете разработку — все компоненты под рукой.

  • Ускоряете деплой — одна команда запускает весь проект.

  • Повышаете безопасность — изоляция и минимальные права сводят риски к минимуму.

Это как конструктор: каждый модуль на своём месте, а Docker — инструкция по сборке. Начните с малого, и вы быстро освоите этот подход!

1
Начать дискуссию