Edit

+serverEntry

Environment: server
Global

+serverEntry.js enables you to control how the server starts. You can define it either:

  • With +server.js => +serverEntry.js wraps your +server.js in production.
  • Without +server.js => +serverEntry.js defines the entire server (for both development and production) — it gives you more control than +server.js.

See explanation at: With(out) +server.js.

Use case examples:

  • Deploy to a platform without built-in support, e.g. Example: AWS Lambda
  • Use a server framework without built-in support (e.g. Koa or hapi)
  • Use non-Node.js server APIs, such as Bun.serve() or Deno.serve(), see Example: Bun
  • Use WebSockets with any library (e.g. Socket.IO), see Example: WebSockets
  • Use all CPU cores with Node.js's cluster module

See also:

If you don't define +serverEntry.js:

  • In development, Vite's built-in development server acts as the entry.
  • In production, Vike's built-in server entry uses srvx (which has first-class support for Node.js, Bun, and Deno).

With(out) +server.js

With +server.js

If you define both +serverEntry.js and +server.js, then +serverEntry.js wraps +server.js and is only used for production.

In development, Vite's development server acts as the entry: your +server.js is attached to Vite's development server. Consequently, +serverEntry.js isn't used in development. In other words: +serverEntry.js never runs in development.

The typical use case is to customize a deployment integration, see Example: AWS Lambda.

// +serverEntry.js
 
// Wrapping +server.js — only in production
 
// Your +server.js exports
import server from 'vike:server'
 
// startServer() can be anything, typically a deployment tool
startServer({
  port: 3000,
  onRequest: (request) => server.fetch(request)
})
// +serverEntry.ts
 
// Wrapping +server.ts — only in production
 
// Your +server.js exports
import server from 'vike:server'
 
// startServer() can be anything, typically a deployment tool
startServer({
  port: 3000,
  onRequest: (request) => server.fetch(request)
})
# Build: +serverEntry.ts => dist/server/index.mjs
$ vike build
# Run +serverEntry.ts
$ node dist/server/index.mjs

Without +server.js

If you don't define +server.js, then +serverEntry.js defines the entire server (for both development and production).

// +serverEntry.js
 
// Defines the entire server — both development and production
 
import express from 'express'
import vike from '@vikejs/express'
 
const app = express()
vike(app) // Make sure to run vike(app) before your routes
 
// ...
// Your routes
// ...
 
// Unlike +server.js, note how +serverEntry.js determines how the server starts:
app.listen(3000)
// +serverEntry.ts
 
// Defines the entire server — both development and production
 
import express from 'express'
import vike from '@vikejs/express'
 
const app = express()
vike(app) // Make sure to run vike(app) before your routes
 
// ...
// Your routes
// ...
 
// Unlike +server.js, note how +serverEntry.js determines how the server starts:
app.listen(3000)

See also full example using renderPage().

+serverEntry.js is responsible for starting the server, whereas with +server.js the server is started by Vite in development and by srvx in production.

In development, when you modify +serverEntry.js (or a file it imports), Vike automatically restarts your server process.

If you have the choice, we recommend +server.js: it has a better development experience, because modifying your server code doesn't restart your server, see:

If you want to use HTTPS in development, use HTTPS in your server and also pass the HTTPS certificates to Vite (Vite uses them for its HMR WebSocket).

# Build: +serverEntry.ts => dist/server/index.mjs
$ vike build
# Run +serverEntry.ts
$ node dist/server/index.mjs

Example: AWS Lambda

AWS Lambda calls the handler() function exported by dist/server/index.mjs. You can implement it natively or with srvx.

The examples below use +serverEntry.js with +server.js, which means that +serverEntry.js is only used in production — see With(out) +server.js.

With AWS Lambda, you usually serve the static assets dist/client/ with a CDN, such as Amazon CloudFront.

Native

// +serverEntry.js (with +server.js)
 
import server from 'vike:server'
 
// AWS Lambda calls handler() for each request (Lambda function URL or API Gateway HTTP API)
export const handler = async (event) => {
  // AWS Lambda event => Request
  const url = new URL(`https://${event.requestContext.domainName}${event.rawPath}`)
  url.search = event.rawQueryString
  const headers = new Headers(event.headers)
  if (event.cookies) headers.set('cookie', event.cookies.join('; '))
  const body = event.body
    ? Buffer.from(event.body, event.isBase64Encoded ? 'base64' : 'utf8')
    : undefined
  const request = new Request(url, { method: event.requestContext.http.method, headers, body })
 
  // Your +server.js exports
  const response = await server.fetch(request)
 
  // Response => AWS Lambda result
  return {
    statusCode: response.status,
    headers: Object.fromEntries([...response.headers].filter(([name]) => name !== 'set-cookie')),
    cookies: response.headers.getSetCookie(),
    body: Buffer.from(await response.arrayBuffer()).toString('base64'),
    isBase64Encoded: true
  }
}
// +serverEntry.ts (with +server.js)
 
import type { APIGatewayProxyHandlerV2 } from 'aws-lambda'
import server from 'vike:server'
 
// AWS Lambda calls handler() for each request (Lambda function URL or API Gateway HTTP API)
export const handler: APIGatewayProxyHandlerV2 = async (event) => {
  // AWS Lambda event => Request
  const url = new URL(`https://${event.requestContext.domainName}${event.rawPath}`)
  url.search = event.rawQueryString
  const headers = new Headers(event.headers as Record<string, string>)
  if (event.cookies) headers.set('cookie', event.cookies.join('; '))
  const body = event.body ? Buffer.from(event.body, event.isBase64Encoded ? 'base64' : 'utf8') : undefined
  const request = new Request(url, { method: event.requestContext.http.method, headers, body })
 
  // Your +server.js exports
  const response = await server.fetch(request)
 
  // Response => AWS Lambda result
  return {
    statusCode: response.status,
    headers: Object.fromEntries([...response.headers].filter(([name]) => name !== 'set-cookie')),
    cookies: response.headers.getSetCookie(),
    body: Buffer.from(await response.arrayBuffer()).toString('base64'),
    isBase64Encoded: true
  }
}

srvx

// +serverEntry.js (with +server.js)
 
import { toLambdaHandler } from 'srvx/aws-lambda'
import server from 'vike:server'
 
// AWS Lambda calls handler() for each request
export const handler = toLambdaHandler({ fetch: server.fetch })
// +serverEntry.ts (with +server.js)
 
import { toLambdaHandler } from 'srvx/aws-lambda'
import server from 'vike:server'
 
// AWS Lambda calls handler() for each request
export const handler = toLambdaHandler({ fetch: server.fetch })

Example: Bun

In production, without +serverEntry.js, $ bun dist/server/index.mjs runs Vike's built-in server entry powered by srvx, which automatically uses Bun.serve() under the hood. In development, $ vike dev uses Vite's development server which uses the Node.js server APIs and not Bun.serve().

With +serverEntry.js (and without +server.js, see With(out) +server.js), you can use Bun.serve() in both production and development. This allows you to, for example, use Bun's WebSockets:

// +serverEntry.js (without +server.js)
 
import { getGlobalContext } from 'vike'
import { Hono } from 'hono'
import vike from '@vikejs/hono'
import { fetchNodeHandler } from 'srvx/node'
import assert from 'node:assert'
 
const globalContext = await getGlobalContext()
 
const app = new Hono()
vike(app)
 
Bun.serve({
  port: 3000,
  async fetch(request) {
    if (import.meta.env.DEV) {
      const { devMiddleware } = globalContext
      assert(devMiddleware)
      let handled = true
      // devMiddleware is Vite's development middleware, which is a Connect middleware.
      // We use fetchNodeHandler() to convert it to the web-standard Request/Response API.
      const response = await fetchNodeHandler(
        (req, res, next) =>
          devMiddleware(req, res, () => {
            handled = false
            next?.()
          }),
        request
      )
      if (handled) return response
    } else {
      // Serve the static assets (dist/client/)
      const file = Bun.file(`${import.meta.dirname}/../client${new URL(request.url).pathname}`)
      if (await file.exists()) return new Response(file)
    }
    return app.fetch(request)
  }
})
// +serverEntry.ts (without +server.js)
 
import { getGlobalContext } from 'vike'
import { Hono } from 'hono'
import vike from '@vikejs/hono'
import { fetchNodeHandler } from 'srvx/node'
import assert from 'node:assert'
import type { IncomingMessage, ServerResponse } from 'node:http'
 
const globalContext = await getGlobalContext()
 
const app = new Hono()
vike(app)
 
Bun.serve({
  port: 3000,
  async fetch(request) {
    if (import.meta.env.DEV) {
      const { devMiddleware } = globalContext
      assert(devMiddleware)
      let handled = true
      // devMiddleware is Vite's development middleware, which is a Connect middleware.
      // We use fetchNodeHandler() to convert it to the web-standard Request/Response API.
      const response = await fetchNodeHandler(
        (req: IncomingMessage, res: ServerResponse, next?: () => void) =>
          devMiddleware(req, res, () => {
            handled = false
            next?.()
          }),
        request
      )
      if (handled) return response
    } else {
      // Serve the static assets (dist/client/)
      const file = Bun.file(`${import.meta.dirname}/../client${new URL(request.url).pathname}`)
      if (await file.exists()) return new Response(file)
    }
    return app.fetch(request)
  }
})

Vite's development middleware is a Connect middleware (it uses Node.js's req/res), which is why it needs a bridge such as srvx's fetchNodeHandler(). Vite wants to move away from Connect, see vitejs/ecosystem#16.

# Development (with package.json#scripts.dev set to "vike dev")
$ bun --bun run dev
# Production
$ vike build
$ bun dist/server/index.mjs

Deno.serve() works the same way.

Example: WebSockets

Using +serverEntry.js (without +server.js), you can use WebSockets like with any Node.js server, in both development and production. For example with ws:

// +serverEntry.js (without +server.js)
 
import express from 'express'
import vike from '@vikejs/express'
import { WebSocketServer } from 'ws'
import { getGlobalContext } from 'vike'
 
const app = express()
 
// Vite's development middleware, or the static assets (dist/client/) in production
const { devMiddleware } = await getGlobalContext()
app.use(devMiddleware ?? express.static(`${import.meta.dirname}/../client`))
 
vike(app)
 
const server = app.listen(3000)
 
// WebSockets — the same code in development and production
const wss = new WebSocketServer({ server, path: '/ws' })
wss.on('connection', (ws) => {
  ws.on('message', (message) => ws.send(`echo: ${message}`))
})
// +serverEntry.ts (without +server.js)
 
import express from 'express'
import vike from '@vikejs/express'
import { WebSocketServer } from 'ws'
import { getGlobalContext } from 'vike'
 
const app = express()
 
// Vite's development middleware, or the static assets (dist/client/) in production
const { devMiddleware } = await getGlobalContext()
app.use(devMiddleware ?? express.static(`${import.meta.dirname}/../client`))
 
vike(app)
 
const server = app.listen(3000)
 
// WebSockets — the same code in development and production
const wss = new WebSocketServer({ server, path: '/ws' })
wss.on('connection', (ws) => {
  ws.on('message', (message) => ws.send(`echo: ${message}`))
})

In development, Vite's HMR uses its own WebSocket server (port 24678 by default), so it doesn't interfere with your WebSockets.

// Client-side
 
const ws = new WebSocket(`${location.protocol === 'https:' ? 'wss:' : 'ws:'}//${location.host}/ws`)
ws.onopen = () => ws.send('hello')
ws.onmessage = (event) => console.log(event.data) // echo: hello

Compared to WebSockets with +server.js:

  • You don't need a Vite plugin: +serverEntry.js defines the entire server (for both development and production), so the same WebSockets integration runs in both development and production.
  • You can use any library that works with Node.js's HTTP server, for example Socket.IO (including its HTTP long-polling fallback).

That said, if you have the choice, we recommend +server.js: it has a better development experience, because modifying your server code doesn't restart your server, see:

With Bun, you can use the WebSockets built into Bun.serve(), in both development and production. For example, modifying the Bun example above:

// +serverEntry.js (without +server.js)
 
// ...
 
Bun.serve({
  port: 3000,
  async fetch(request) { 
  async fetch(request, server) {
    if (new URL(request.url).pathname === '/ws' && server.upgrade(request)) return
    // ...
  },
  websocket: {
    message(ws, message) {
      ws.send(`echo: ${message}`) 
    } 
  } 
})
// +serverEntry.ts (without +server.js)
 
// ...
 
Bun.serve({
  port: 3000,
  async fetch(request) { 
  async fetch(request, server) { 
    if (new URL(request.url).pathname === '/ws' && server.upgrade(request)) return
    // ...
  },
  websocket: { 
    message(ws, message) { 
      ws.send(`echo: ${message}`) 
    } 
  } 
})

vike:server

import server from 'vike:server' is your +server.js exports (e.g. { fetch }).

If you don't define +server.js, then it's the exports of Vike's built-in server.

It's typically used for wrapping +server.js, for example:

// +serverEntry.js
 
import { serve } from 'srvx'
import server from 'vike:server'
 
serve({
  port: 3000,
  fetch(request) {
    if (new URL(request.url).pathname === '/health') return new Response('OK')
    return server.fetch(request)
  }
})
// +serverEntry.ts
 
import { serve } from 'srvx'
import server from 'vike:server'
 
serve({
  port: 3000,
  fetch(request) {
    if (new URL(request.url).pathname === '/health') return new Response('OK')
    return server.fetch(request)
  }
})

Static assets

When using +serverEntry.js, it's your responsibility to serve the static assets dist/client/.

If you don't use +serverEntry.js, then Vike's built-in server automatically serves the static assets.

See +precompress to generate .br/.gz files for your static assets at build time.

Pre-rendering

Pre-rendering doesn't execute +serverEntry.js.

If all your pages are pre-rendered, dist/server/ is still kept (Vike otherwise removes it, see API > +prerender > keepDistServer).

See also