Building a Production-Ready SpringBoot + Vue3 Front‑End/Back‑End Separation Architecture

This article presents a step‑by‑step guide to constructing a full‑stack SpringBoot + Vue3 project with production‑grade features such as global exception handling, unified error codes, JWT authentication, multi‑environment configuration, CORS solutions, pagination, Pinia state management, on‑demand UI imports, and Nginx reverse‑proxy deployment.

Java Tech Workshop
Java Tech Workshop
Java Tech Workshop
Building a Production-Ready SpringBoot + Vue3 Front‑End/Back‑End Separation Architecture

Overall Architecture Overview

Typical front‑end/back‑end separation tutorials stop at basic API integration and omit production concerns such as global exception handling, unified error codes, Token authentication, front‑end state management, multi‑environment configuration, CORS fundamentals, and online troubleshooting. This guide provides the missing pieces.

1.1 Browser Same‑Origin Policy and CORS Fundamentals

Same‑origin requires identical protocol, domain, and port. CORS is not a request failure; the browser blocks the response after the server returns data. Two request categories exist:

Simple request: GET/POST/HEAD, no custom headers, Content‑Type limited to text/plain or application/x-www-form-urlencoded.

Pre‑flight request (OPTIONS): carries Token, JSON body, custom headers; the browser sends an OPTIONS request first to verify permission, which is the common cause of Token‑related CORS errors.

1.2 Front‑End / Back‑End Division of Responsibilities

Back‑end: parameter validation, DB transactions, permission checks, data masking, rate limiting, logging, global exception handling, JWT issuance and verification.

Front‑end: parameter validation, route guards, Token storage, form interaction, page caching, debounce/throttle, static resource compression.

1.3 Technology Stack

Back‑end

SpringBoot 2.7.15, Maven 3.8, MySQL 8.0, MyBatis‑Plus 3.5.3.1, JWT 0.9.1, Lombok, Spring Validation, global exception handler, Knife4j API documentation.

Front‑end

Vue 3 (Composition API setup syntax), Vite 5, VueRouter 4, Axios 1.7, Pinia 2, Element Plus, js‑cookie, nprogress.

2. SpringBoot Back‑End Production‑Level Construction

2.1 Standardised Directory Structure

backend
├── config        // global config (CORS, MVC, JWT, MyBatis‑Plus)
├── controller    // API controllers
├── service       // service layer
│   └── impl      // service implementations
├── mapper        // MyBatis mapper interfaces
├── entity        // DB entities
├── dto           // request DTOs
├── vo            // response VOs
├── exception     // custom business exceptions
├── handler       // global handlers (exception, AOP)
├── util          // utilities (JWT, date, encryption)
└── resources
    ├── mapper   // MyBatis XML files
    ├── application-dev.yml
    ├── application-test.yml
    └── application-prod.yml

2.2 Multi‑Environment YML Separation

Disable single‑file configuration and split into dev, test, prod profiles to avoid configuration errors when going live.

Base application.yml:

spring:
  profiles:
    active: dev   # switch dev/test/prod
  jackson:
    date-format: yyyy-MM-ddHH:mm:ss
    time-zone: Asia/Shanghai
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.separate.entity
  configuration:
    map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl   # dev prints SQL
application-dev.yml

(enables CORS, SQL logging, Knife4j):

server:
  port: 8080
  servlet:
    context-path: /api
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/separate_db?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&allowMultiQueries=true
    username: root
    password: root
knife4j:
  enable: true   # enable API docs in dev
application-prod.yml

(disables docs, CORS, SQL logging, points to production DB):

server:
  port: 8080
  servlet:
    context-path: /api
spring:
  datasource:
    url: jdbc:mysql://172.16.0.10:3306/separate_db
knife4j:
  enable: false
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.nologging.NoLoggingImpl

2.3 Custom Business Exception + Global Exception Handler

Replace scattered try‑catch blocks with a unified interceptor that returns a standardised response.

@Data
public class BusinessException extends RuntimeException {
    private Integer code;
    private String msg;
    public BusinessException(ResultCode resultCode) {
        super(resultCode.getMsg());
        this.code = resultCode.getCode();
        this.msg = resultCode.getMsg();
    }
    public BusinessException(Integer code, String msg) {
        super(msg);
        this.code = code;
        this.msg = msg;
    }
}
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    @ExceptionHandler(BusinessException.class)
    public Result<?> businessException(BusinessException e){
        log.error("业务异常:{}", e.getMsg());
        return Result.fail(e.getCode(), e.getMsg());
    }
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<?> paramException(MethodArgumentNotValidException e){
        String msg = e.getBindingResult().getFieldError().getDefaultMessage();
        return Result.fail(400, "参数校验失败:" + msg);
    }
    @ExceptionHandler(Exception.class)
    public Result<?> sysException(Exception e){
        log.error("系统未知异常", e);
        return Result.fail(500, "服务器内部异常,请稍后重试");
    }
}

2.4 CORS Configuration Fix

The original configuration could not handle Token pre‑flight requests. The new CorsConfig allows all custom headers and caches OPTIONS for one hour.

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("http://localhost:5173") // dev front‑end address
                .allowedMethods("GET","POST","PUT","DELETE","OPTIONS")
                .allowedHeaders("*") // allow all custom headers (Token)
                .allowCredentials(true)
                .maxAge(3600); // cache pre‑flight for 1 hour
    }
}

2.5 MyBatis‑Plus Pagination Plugin

Enterprise APIs often require pagination; the interceptor adds a pagination plugin and a block‑attack interceptor to prevent full‑table updates.

@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // pagination
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        // block full‑table update/delete
        interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
        return interceptor;
    }
}

2.6 JWT Authentication Implementation

public class JwtUtil {
    private static final String SECRET_KEY = "separate-jwt-secret-2026";
    private static final long EXPIRE_TIME = 2*60*60*1000; // 2 hours

    public static String generateToken(Long userId){
        Date now = new Date();
        Date expireDate = new Date(now.getTime() + EXPIRE_TIME);
        return Jwts.builder()
                .setSubject(userId.toString())
                .setIssuedAt(now)
                .setExpiration(expireDate)
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
                .compact();
    }

    public static Long getUserIdByToken(String token){
        Claims claims = Jwts.parser()
                .setSigningKey(SECRET_KEY)
                .parseClaimsJws(token)
                .getBody();
        return Long.parseLong(claims.getSubject());
    }
}

Login endpoint example:

@PostMapping("/login")
public Result<String> login(@RequestBody UserLoginDTO dto){
    User user = userService.lambdaQuery()
            .eq(User::getUsername, dto.getUsername())
            .eq(User::getPassword, dto.getPassword())
            .one();
    if (Objects.isNull(user)){
        throw new BusinessException(401, "账号密码错误");
    }
    String token = JwtUtil.generateToken(user.getId());
    return Result.success(token);
}

3. Vue 3 + Vite Front‑End Production Enhancements

3.1 Standardised Front‑End Directory

src
├── api          // module APIs (user, test, login)
├── assets       // static images, global styles
├── components   // global UI components (dialog, pagination)
├── router       // routes and guards
├── store        // Pinia store (token, user info)
├── utils        // axios, cookie, date helpers
├── views        // business pages
├── directive    // custom directives (button permission)
└── env          // environment variable files

3.2 Multi‑Environment Variable Configuration

.env.development – backend address http://localhost:8080/api.env.test – backend address 192.168.x.x:8080.env.production – backend address /api (Nginx forward)

Vite automatically picks the appropriate file.

VITE_BASE_URL=http://localhost:8080/api

Axios base URL reads the variable:

const service = axios.create({
  baseURL: import.meta.env.VITE_BASE_URL,
  timeout: 5000
});

3.3 Axios Full‑Duplex Interceptor (Token, 401/403 handling, progress bar)

import axios from 'axios';
import nprogress from 'nprogress';
import 'nprogress/nprogress.css';
import { useUserStore } from '@/store/user';
import router from '@/router';

const service = axios.create({
  baseURL: import.meta.env.VITE_BASE_URL,
  timeout: 5000
});

// request interceptor – attach Token
service.interceptors.request.use(config => {
  nprogress.start();
  const userStore = useUserStore();
  if (userStore.token) {
    config.headers.Authorization = `Bearer ${userStore.token}`;
  }
  return config;
});

// response interceptor – unified status handling
service.interceptors.response.use(res => {
  nprogress.done();
  const data = res.data;
  if (data.code === 401) {
    const userStore = useUserStore();
    userStore.logout();
    router.push('/login');
    ElMessage.error('登录已过期,请重新登录');
    return Promise.reject(data);
  }
  if (data.code !== 200) {
    ElMessage.error(data.msg);
    return Promise.reject(data);
  }
  return data;
}, err => {
  nprogress.done();
  ElMessage.error('服务器异常');
  return Promise.reject(err);
});

export default service;

3.4 Pinia Global State Management

Replace Vuex; stores Token and user info with js-cookie persistence.

npm install pinia js-cookie
import { defineStore } from 'pinia';
import Cookies from 'js-cookie';

export const useUserStore = defineStore('user', {
  state: () => ({
    token: Cookies.get('token') || '',
    username: Cookies.get('username') || ''
  }),
  actions: {
    // save login info
    saveUser(token, username) {
      this.token = token;
      this.username = username;
      Cookies.set('token', token, { expires: 2 }); // 2 days
      Cookies.set('username', username, { expires: 2 });
    },
    // logout
    logout() {
      this.token = '';
      this.username = '';
      Cookies.remove('token');
      Cookies.remove('username');
    }
  }
});

3.5 Router Global Guard (Unauthenticated Access Block)

router.beforeEach((to, from, next) => {
  const userStore = useUserStore();
  if (to.path !== '/login' && !userStore.token) {
    next('/login');
    ElMessage.warning('请先登录');
  } else {
    next();
  }
});

3.6 Element Plus On‑Demand Import

Using unplugin-vue-components reduces bundle size by ~40 %.

npm i element-plus unplugin-vue-components unplugin-auto-import
import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
export default defineConfig({
  plugins: [
    vue(),
    AutoImport({ resolvers: [ElementPlusResolver()] }),
    Components({ resolvers: [ElementPlusResolver()] })
  ]
});

4. Cross‑Domain Solution Comparison

Backend Global CORS – Simple to configure, suitable for local development; must be disabled in production due to security risks.

Vite Front‑End Proxy – No backend changes, works only locally; not applicable to production.

Nginx Reverse Proxy – Unified domain eliminates CORS entirely; recommended for production.

Policy: use Vite proxy in development, switch to Nginx reverse proxy for test/production, and permanently disable backend CORS in online environments.

5. Nginx Deployment Configuration

server {
    listen 80;
    server_name web.separate.com;

    # Front‑end static files
    location / {
        root /usr/local/nginx/html/dist;
        index index.html;
        try_files $uri $uri/ /index.html; # handle history mode
    }

    # Backend API forwarding
    location /api/ {
        proxy_pass http://127.0.0.1:8080/api/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # Static resource caching
    location ~* \.(jpg|png|css|js)$ {
        expires 7d;
    }
}

The complete architecture satisfies enterprise‑grade standards: multi‑environment isolation, global exception handling, JWT authentication, front‑end route guard, Pinia persistence, on‑demand bundling, same‑origin Nginx deployment, and online troubleshooting. It can serve as a ready‑made scaffold for small‑to‑medium management systems, backend APIs, and mobile back‑ends.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

corsNginxSpringBootMyBatis-Plusjwtpiniavue3
Java Tech Workshop
Written by

Java Tech Workshop

Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.