Building a Server-Side Rendered (SSR) app involves orchestrating three main components: the frontend app, the SSR render entry, and the server to serve assets and handle backend APIs. In this guide, we'll leverage Vite for the frontend, Node.js and Express for the server, and demonstrate a streamlined development workflow using Nx. Prerequisites- Node.js and npm installed
- Basic understanding of JavaScript, React.js (or your preferred frontend framework), and Express.js
Project StructureBefore diving into the setup, let's outline the project structure: frontend app src/main.tsx SSR src/main.server.tsx backend app src/server.ts 3 vite configs: vite.config.ts for frontend vite.config.ssr.ts for SSR vite.config.server.ts for backend project.json: define build targets you can checkout the full example from github. vite.config.ts fronend app config just a normal Vite default config. export default defineConfig({ plugins: [ react(), // import your frontend framework plugin nxViteTsPaths(), ], }); vite.config.ssr.ts SSR config export default defineConfig({ plugins: [ react(), //yes, we need the same plugin here nxViteTsPaths(), ], build: { target: "esnext", rollupOptions: { external: ["fsevents", "esbuild", "vite", "fs", "path", "https", "http", "tls", "net", "zlib", "stream", "tty", "os", "crypto", "util"], output: { dynamicImportInCjs: true } }, copyPublicDir: false, // not copy public assets minify: false }, optimizeDeps: { esbuildOptions: { target: "esnext" }, force: true }, ssr: { // for production standalone deployment. bundle everything except the above external // so that await import main.server.mjs no need any dpes under node_modules noExternal: process.env["NX_TASK_TARGET_CONFIGURATION"] === "production" ? true : undefined } }); vite.config.server.ts backend server config same as vite.config.ssr.ts, but remove frontend plugin. Perhaps more externals need to specify not bundle for production build. Project Build Structure: project.jsonproduction build, depends on 3 parts "build": { "dependsOn": ["browser-build", "ssr-build", "server-build"], "executor": "nx:run-commands", "options": { "commands": [{ "command":"echo 'build done'" }] } }, browser build "browser-build": { "executor": "@nx/vite:build", "outputs": ["{options.outputPath}"], "defaultConfiguration": "production", "options": { "outputPath": "dist/apps/activate/browser" }, "configurations": { "development": { "mode": "development" }, "production": { "mode": "production" } } }, SSR build: use ssr and specify configFile "emptyOutDir" needs set to false, since we are using the same output path for backend entry. "ssr-build": { "executor": "@nx/vite:build", "outputs": ["{options.outputPath}"], "defaultConfiguration": "production", "options": { "emptyOutDir": false, "outputPath": "dist/apps/activate/server", "ssr": "src/main.server.tsx", "configFile": "apps/activate/vite.config.ssr.ts" }, "configurations": { "development": { "mode": "development" }, "production": { "mode": "production" } } }, Backend Build similar to SSR, use different configFile. We also need to specify the "outputFileName" as well, otherwise you will get an error while work with @nx/js:node executor. "server-build": { "executor": "@nx/vite:build", "outputs": ["{options.outputPath}"], "defaultConfiguration": "production", "options": { "outputPath": "dist/apps/activate/server", "ssr": "src/server.ts", "configFile": "apps/activate/vite.config.server.ts", "emptyOutDir": false, "outputFileName": "server.mjs" }, "configurations": { "development": { "mode": "development" }, "production": { "mode": "production" } } },
Server.tsSet SSR with Vite, please refer to https://vitejs.dev/guide/ssr. This server.ts is doing exactly same follow the doc. app.use('*', async (req, res) => { let template; if (process.env["NODE_ENV"] === "production") { template = fs.readFileSync(path.resolve(fileURLToPath(import.meta.url), '../../browser/index.html')).toString(); const { ssrRender } = await import(path.resolve(fileURLToPath(import.meta.url), '../main.server.mjs')); ssrRender(template, req, res); res.status(200); } else { const url = req.originalUrl // always read fresh template in dev template = fs.readFileSync(path.resolve(vite.config.root, './index.html'), 'utf-8') template = await vite.transformIndexHtml(url, template) const { ssrRender } = await vite.ssrLoadModule('/src/main.server.tsx'); ssrRender(template, req, res); } })
ServeLet's server our SSR app. "serve": { "executor": "@nx/js:node", "defaultConfiguration": "development", "options": { "buildTarget": "activate:build" }, "configurations": { "development": { "buildTarget": "activate:server-build:development" }, "production": { "buildTarget": "activate:server-build:production", "waitUntilTargets": ["activate:browser-build:production", "activate:ssr-build:production"] } } },
As you can observe, for development, you just need compile one file: server.ts. The server just started in less than 50ms. All the frontend part will be take over by Vite Dev Server and provide fast build, HMR and more.
ConclusionCongratulations! You've successfully set up a Server-Side Rendered (SSR) app with Vite and Express, creating a powerful and efficient web application with benefits on both the frontend and backend. By leveraging Vite's capabilities for rapid frontend development, you've harnessed features like Hot Module Replacement (HMR) for instantaneous updates during development. Combining this with an Express server allows you to achieve server-side rendering, providing improved performance and search engine optimization. The use of Nx has streamlined your development workflow, enabling watch mode for both frontend and backend applications. This ensures that any changes made to your code trigger automatic rebuilds, enhancing productivity and reducing development time. As you move forward, consider exploring additional features and optimizations for your SSR app, such as: - Optimizing SSR Render Entry: Fine-tune the SSR render entry point for performance improvements and better server-side rendering results.
- Middleware and API Enhancements: Extend your Express server with middleware for additional functionalities and enhance your backend APIs to meet the requirements of your application.
- Testing and Deployment Strategies: Implement thorough testing procedures for both the frontend and backend components. Explore deployment options, such as containerization or serverless architectures, based on your application's needs.
Remember to refer to official documentation and community resources for ongoing support and updates on the tools and frameworks you've used in this setup. Now, with a robust SSR app in place, you're well-equipped to deliver a seamless user experience while taking advantage of the best features that Vite and Express have to offer. Happy coding! |