Implementing Frontend‑Backend Integration with Vite Plugin: A Step‑by‑Step Guide
This article explains the concept of frontend‑backend integration, demonstrates how Vite can transform TypeScript functions into API requests without explicit AJAX calls, and provides a complete plugin implementation that intercepts, forwards, and handles these requests using Express and middleware.
While browsing GitHub the author discovered the midwayjs and ByteDance modern.js projects, both of which emphasize a seamless frontend‑backend integration that eliminates the need for explicit AJAX calls. Inspired by the lack of documentation, the author writes a tutorial to illustrate the idea.
What Is Frontend‑Backend Integration?
Using the example from the Modern.js website, a simple API file and a React component are shown:
// api/hello.ts
export const get = async () => "Hello Modern.js"; // src/App.tsx
import { useState, useEffect } from "react";
import { get as hello } from "@api/hello";
export default () => {
const [text, setText] = useState("");
useEffect(() => { hello().then(setText); }, []);
return
{text}
;
};When the page is opened, the network panel shows a request to http://localhost:8080/api/hello that returns the string defined in the API file.
Although there is no official definition, the author proposes a simple one:
Frontend code and Node.js backend code reside in the same project.
The same package.json manages all dependencies.
Both sides interact via direct function calls instead of traditional AJAX requests.
Why Use Frontend‑Backend Integration?
Two main benefits are highlighted:
Unified Types : When using Node + TypeScript, type definitions only need to be written once and can be shared between server and client, avoiding duplication or custom synchronization tools.
Simplified Development : Calls to backend functions look like ordinary function invocations, removing the need for explicit routes, GET/POST handling, and AJAX boilerplate, which greatly improves developer experience.
FAQ
What’s the difference from traditional non‑separated front‑end/back‑end?
The integration still results in two bundles (frontend and backend) and ultimately uses HTTP requests, unlike classic server‑side rendering that injects HTML strings.
It does not restrict the frontend stack; it merely wraps AJAX calls, hiding them from the developer.
Applicable Scenarios
The two referenced projects target serverless use‑cases where each function maps to an endpoint, but they can also be deployed independently.
Principle and Implementation
Principle
The magic lies in the apis directory being read twice: once at build time to convert functions into request code, and once at runtime to serve those functions as route handlers.
Implementation
The tutorial uses vite as the build tool (easier to understand than a Webpack plugin) and walks through a minimal implementation.
1. Initialize Project
yarn create @vitejs/app my-vue-app --template vue-ts cd my-vue-app
yarn
yarn devAfter running the commands the development server starts successfully.
2. Add New Files
// src/apis/user.ts
export interface User {
name: string;
age: number;
}
interface Response
{
code: number;
msg: string;
data?: T;
}
export async function getUser(): Promise
> {
// Simulate DB read
const user: User = { name: "jack", age: 18 };
return { code: 0, msg: "ok", data: user };
}
export async function updateUser(user: User): Promise
> {
return { code: 0, msg: "ok", data: user };
} <script setup lang="ts">
import { onMounted, ref } from "vue";
import { getUser, User, updateUser } from "./apis/user";
const user = ref<User>();
onMounted(() => {
getUser().then(res => { user.value = res.data; });
});
const handleUpdate = () => {
updateUser({ name: "li", age: 10 }).then(res => {
alert(JSON.stringify(res.data));
});
};
</script>
<template>
<div v-if="user">
<div>username: {{ user.name }}</div>
<div>age: {{ user.age }}</div>
<button @click="handleUpdate">Update user</button>
</div>
</template>Opening the network panel shows that no AJAX request is sent yet, indicating further transformation is required.
3. Transform Functions into API Requests
A Vite plugin is created to rewrite files under src/apis :
// myPlugin.ts
import { Plugin } from "vite";
export default function VitePlugin(): Plugin {
return {
name: "my-plugin",
transform(src, id) {
// src is file content, id is file path
},
};
} // vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import myPlugin from "./myPlugin";
export default defineConfig({
plugins: [vue(), myPlugin()],
});The core logic performs two steps:
Detect files under src/apis .
Replace each async function with a request wrapper that calls fetch using a generated URL pattern.
// Target transformation
function getUser() {
// 1. Use fetch
// 2. URL = /api/
/
return fetch("/api/user/getUser", {
method: "GET",
headers: { "Content-Type": "application/json" },
}).then(res => res.json());
}
function updateUser(data) {
return fetch("/api/user/updateUser", {
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
}).then(res => res.json());
}4. Intercept Requests
Vite provides a configureServer hook. The plugin registers a middleware that forwards /api calls to a separate Express server:
export default function VitePlugin(): Plugin {
return {
name: "my-plugin",
async configureServer(server) {
server.middlewares.use(bodyParser.json());
server.middlewares.use(await middleware());
},
};
}The middleware function starts the Express app, forwards the request via axios , and returns the response.
5. Start Express Service
// Install dependencies
yarn add express body-parser # express
yarn add ts-node # run TS files in Node
yarn add axios # forward requests import * as fs from "fs";
import * as path from "path";
require("ts-node/register");
function getApis() { /* read src/apis, require each TS file, return {fileName, fn} */ }
function registerApis(server: Express) { /* map each fn to /api/
/
*/ }
async function appStart(): Promise
{ /* start Express on random port and return address */ }
function sendRequest(address: string, url: string, body: any, params: any) { /* axios POST */ }
async function middleware() { /* launch app, return (req,res,next) => { if(req.url.startsWith('/api')) { forward } } }6. Final Plugin Code
import * as path from "path";
import * as fs from "fs";
import * as http from "http";
import express from "express";
import axios from "axios";
import { Plugin } from "vite";
import { Express } from "./node_modules/@types/express-serve-static-core/index";
require("ts-node/register");
const bodyParser = require("body-parser");
const apisPath = path.join(__dirname, "./src/apis");
const requestTemp = (fileName: string, fn: string) => `export function ${fn}(data) {
const isGet = !data;
return fetch("/api/${fileName}/${fn}", {
method: isGet ? "GET" : "POST",
body: isGet ? undefined : JSON.stringify(data),
headers: { 'Content-Type': 'application/json' },
}).then(res => res.json());
}`;
function getApis() { /* same as above */ }
function registerApis(server: Express) { /* same as above */ }
function appStart(): Promise
{ /* same as above */ }
function sendRequest(address: string, url: string, body: any, params: any) { /* same as above */ }
async function middleware() { /* same as above */ }
function transformRequest(src: string, id: string) {
if (id.startsWith(apisPath)) {
const fileName = path.basename(id, ".ts");
const fnNames = [...src.matchAll(/async function (\w+)/g)].map(item => item[1]);
return { code: fnNames.map(fn => requestTemp(fileName, fn)).join("\n"), map: null };
}
}
export default function VitePlugin(): Plugin {
return {
name: "my-plugin",
transform: transformRequest,
async configureServer(server) {
server.middlewares.use(bodyParser.json());
server.middlewares.use(await middleware());
},
};
}The complete source code is available on GitHub: https://github.com/dream2023/frontend-and-backend-demo .
Conclusion
Frontend‑backend integration dramatically simplifies API consumption by turning function calls into HTTP requests automatically, but it also introduces considerations such as build complexity, deployment strategy, and bundle size. Whether it becomes a lasting paradigm or just a concept depends on future tooling and project requirements.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
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.