Vite is a fast and lightweight build tool for modern JavaScript applications. It is designed to be simple to use and easy to configure, with a focus on providing a smooth developer experience.
We love developing with Vite because it's fast and easy to work with. But we're developing a full-stack app with Express as our API server and running both Vite's dev server and a separate Node process in development is annoying. So we looked for the simplest way to have Vite run the Express server and hot-reload it as we make changes.
Let's demonstrate it by building a full-stack app, that runs simultaneously on the same machine react-ts
and express
.
A Little More About Vite
Vite uses native ES module imports to build your applications and supports hot module replacement (HMR) for fast development. It also has built-in support for JSX and TypeScript, and allows you to use modern syntax such as the optional chaining operator foo?.bar
and nullish coalescing operator null ?? 'bla'
without needing to set up a complicated build configuration.
Overall, Vite aims to make it easy for developers to create high-performance, modern applications with minimal setup and maintenance overhead.
Creating A Simple Vite Project
We will start with the react-ts
template
npm create -y vite@latest my-project-name -- --template react-ts
Adding The Express Server Code
Next, we'll install express
by running
cd my-project-name
npm i express
npm i -D @types/express
After that, we'll create the server's index.ts
file in the src/server
folder
import express from 'express';
export const app = express();
app.get('/api/test', (_, res) =>
res.json({ greeting: "Hello" }
))
Notes:
We didn't add the
app.listen
call - we'll add that later on.We exported the
app
express application - we'll use that later for vite to run.
Creating The vite
Plugin
In the project root create a file called express-plugin.ts
with the following code
export default function express(path: string) {
return {
name: "vite3-plugin-express",
configureServer: async (server: any) => {
server.middlewares.use(async (req: any, res: any, next: any) => {
process.env["VITE"] = "true";
try {
const { app } = await server.ssrLoadModule(path);
app(req, res, next);
} catch (err) {
console.error(err);
}
});
},
};
}
Notes:
This code instructs
vite
to load on its server the path that we send it as a parameter (later on, in our casesrc/server
).It also sets the
VITE
process environment variable totrue
- we can use that to differentiate when the express server is called using this plugin or not. For example, as a part of making our project production-ready, we can add these lines toserver/index.ts
:
if (!process.env['VITE']) {
const frontendFiles = process.cwd() + '/dist'
app.use(express.static(frontendFiles))
app.get('/*', (_, res) => {
res.send(frontendFiles + '/index.html')
})
app.listen(process.env['PORT'])
}
Using The vite
Plugin
Edit the tsconfig.node.json
file to include the express-plugin.ts
file
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts","express-plugin.ts"] // Adjust this
}
Edit the vite.config.ts
file to import the plugin and use it
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import express from './express-plugin' //Add this
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), express('src/server')], // Adjust this
})
Now the express
plugin is being imported from a local file ./express-plugin
. The express
function is called with the argument src/server
specifies the location of the server-side entry file.
When you run vite
with this configuration, it will build the client-side application as usual and then start an Express server in the background. The Express server will use the specified server-side entry file as the starting point for setting up routes, middleware, and any other server-side logic.
Test That It Works
Run the vite
app using npm run dev
.
If you navigate to http://127.0.0.1:5173/
you see the default welcome page of Vite + React
, but our express
server is live too, navigate to http://127.0.0.1:5173/api/test
to see the greeting.
Let's make a quick sanity check for the parallel running of React & Express
. We will add a useEffect hook to get the greeting from our server. In App.tsx
file, add these lines:
//Add this function
const getGreeting = async function () {
const res = await fetch("/api/test");
return await res.json();
};
function App() {
...
const [greeting, setGreeting] = useState(""); // Add this
useEffect(() => { // Add this hook
getGreeting().then((res) => setGreeting(res.greeting));
}, []);
return (
...
//Add this line somewhere
<p>Server response: {greeting}</p>
...
);
}
And on http://127.0.0.1:5173/
you should see the greeting. You don't even need to refresh, thanks to the HMR .
Using An npm Package
To make this easier to use, we've created an npm
package with this plugin called vite3-plugin-express
. This plugin replaces the express-plugin.ts
we built earlier.
Install the package via npm:
npm i vite3-plugin-express
and to use it, slightly change vite.config.ts
file:
//vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import express from "vite3-plugin-express"
export default defineConfig({
plugins: [vue(), express('express-server.ts')]
})
Notes:
Make sure that the express handler is exported as
app
Make sure not to call
app.listen
.The plugin adds the
VITE
environment variable, use that to disable the call toapp.listen
Watch This Video
In this video from our BudapestJS meetup, we showed how to turn a front-end React app into a full-stack app using Node.js, Postgres, and Remult. Of course, we used vite
as a part of our demo. The video's timeline is positioned at the point we configured vite
.