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.
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.yml2.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.NoLoggingImpl2.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 files3.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/apiAxios 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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
