How to Implement SSR with SolidJS 2 and Express 5
Server-side rendering (SSR) improves SEO, initial load performance, and accessibility by rendering application markup on the server before sending it to the client. SolidJS 2’s lightweight reactive model pairs perfectly with Express 5’s modern async/await support and simplified middleware for fast, scalable SSR setups. This guide walks through a complete implementation from scratch.
Prerequisites
- Node.js v20 or later (required for Express 5’s native fetch and async features)
- Basic familiarity with SolidJS components and Express routing
- npm or yarn package manager
Step 1: Initialize the Project
Create a new directory and initialize a Node.js project:
mkdir solid-ssr-express && cd solid-ssr-express
npm init -y
Install core dependencies:
npm install solid-js@2 @solidjs/router@2 @solidjs/ssr express@5
Install dev dependencies for building and transpilation:
npm install -D vite @vitejs/plugin-solid babel-preset-solid
Step 2: Configure Vite for SSR
Create a vite.config.js file in the project root to handle both client and server builds:
import { defineConfig } from 'vite'
import solid from '@vitejs/plugin-solid'
export default defineConfig({
plugins: [solid()],
ssr: {
noExternal: ['solid-js', '@solidjs/router']
},
build: {
ssr: true,
outDir: 'dist/server'
}
})
Add a client-side build script by creating a separate config or adjusting the existing one. For simplicity, we’ll use two build commands: one for the server bundle, one for the client bundle.
Step 3: Create SolidJS Application Files
Create a src directory with the following structure:
src/
components/
App.jsx
pages/
Home.jsx
About.jsx
entry-client.jsx
entry-server.jsx
First, create src/pages/Home.jsx:
import { createSignal } from 'solid-js'
export default function Home() {
const [count, setCount] = createSignal(0)
return (
Home Page
Count: {count()}
setCount(c => c + 1)}>Increment
)
}
Create src/pages/About.jsx:
export default function About() {
return (
About Page
This is a SolidJS 2 app with SSR powered by Express 5.
)
}
Create src/components/App.jsx with client-side routing:
import { Router, Routes, Route } from '@solidjs/router'
import Home from '../pages/Home'
import About from '../pages/About'
export default function App() {
return (
Home
About
)
}
Step 4: Set Up Server Entry Point
Create src/entry-server.jsx to handle server-side rendering logic:
import { renderToString } from '@solidjs/ssr'
import App from './components/App'
export default async function render(url) {
const html = renderToString(() => )
return {
html,
// Add any server-side fetched data here
}
}
Create src/entry-client.jsx for client-side hydration:
import { hydrate } from 'solid-js/web'
import App from './components/App'
hydrate(() => , document.getElementById('root'))
Step 5: Configure Express 5 Server
Create a server.js file in the project root to set up the Express 5 server:
import express from 'express'
import { readFileSync } from 'fs'
import { resolve } from 'path'
import render from './dist/server/entry-server.js'
const app = express()
const port = 3000
// Serve static client assets
app.use(express.static(resolve('dist/client')))
// Handle all other routes with SSR
app.get('*', async (req, res) => {
try {
const { html } = await render(req.url)
const clientBundle = readFileSync(resolve('dist/client/assets/client.js'), 'utf-8')
const fullHtml = `
${html}
`
res.send(fullHtml)
} catch (err) {
console.error(err)
res.status(500).send('Server Error')
}
})
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`)
})
Note: Express 5 uses native async/await support for route handlers, eliminating the need for custom error handling middleware for async errors (though we included a try/catch here for clarity).
Step 6: Build and Run
Add build and start scripts to package.json:
"scripts": {
"build:client": "vite build --outDir dist/client",
"build:server": "vite build --ssr src/entry-server.jsx --outDir dist/server",
"build": "npm run build:client && npm run build:server",
"start": "node server.js"
}
Build the project and start the server:
npm run build
npm start
Navigate to http://localhost:3000 to see the SSR-rendered app. View page source to confirm the initial HTML is rendered on the server.
Step 7: Add Server-Side Data Fetching
To fetch data on the server, modify the entry-server.jsx to pass context to components. For example, update src/pages/Home.jsx to accept server-fetched data:
import { createSignal, useContext } from 'solid-js'
import { ServerDataContext } from '../context/server-data'
export default function Home() {
const [count, setCount] = createSignal(0)
const serverData = useContext(ServerDataContext)
return (
Home Page
Server-fetched message: {serverData?.message}
Count: {count()}
setCount(c => c + 1)}>Increment
)
}
Update the server render function to pass data:
export default async function render(url) {
const serverData = { message: 'Hello from the server!' }
const html = renderToString(() => (
))
return { html, serverData }
}
Best Practices and Troubleshooting
- Use
renderToStringAsyncfrom@solidjs/ssrfor async data fetching during render. - Ensure client-side hydration matches server-rendered markup to avoid mismatches.
- Express 5 removes deprecated features like
app.deland callback-based middleware, so use async/await for all route handlers. - Test SSR output by disabling JavaScript in the browser to confirm content renders correctly.
Conclusion
Implementing SSR with SolidJS 2 and Express 5 combines Solid’s high-performance reactivity with Express’s modern, lightweight server capabilities. This setup delivers fast initial loads, improved SEO, and a seamless client-side experience. Extend this base by adding authentication, API routes, or deployment to platforms like Vercel or AWS Lambda.
Top comments (0)