DEV Community

ANKUSH CHOUDHARY JOHAL
ANKUSH CHOUDHARY JOHAL

Posted on • Originally published at johal.in

How to Implement SSR with SolidJS 2 and Express 5

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
Enter fullscreen mode Exit fullscreen mode

Install core dependencies:

npm install solid-js@2 @solidjs/router@2 @solidjs/ssr express@5
Enter fullscreen mode Exit fullscreen mode

Install dev dependencies for building and transpilation:

npm install -D vite @vitejs/plugin-solid babel-preset-solid
Enter fullscreen mode Exit fullscreen mode

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'
  }
})
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

  )
}
Enter fullscreen mode Exit fullscreen mode

Create src/pages/About.jsx:

export default function About() {
  return (

      About Page
      This is a SolidJS 2 app with SSR powered by Express 5.

  )
}
Enter fullscreen mode Exit fullscreen mode

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






  )
}
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

Create src/entry-client.jsx for client-side hydration:

import { hydrate } from 'solid-js/web'
import App from './components/App'

hydrate(() => , document.getElementById('root'))
Enter fullscreen mode Exit fullscreen mode

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}`)
})
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

Build the project and start the server:

npm run build
npm start
Enter fullscreen mode Exit fullscreen mode

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

  )
}
Enter fullscreen mode Exit fullscreen mode

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 }
}
Enter fullscreen mode Exit fullscreen mode

Best Practices and Troubleshooting

  • Use renderToStringAsync from @solidjs/ssr for async data fetching during render.
  • Ensure client-side hydration matches server-rendered markup to avoid mismatches.
  • Express 5 removes deprecated features like app.del and 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)