+serverEntry
+serverEntry.js enables you to control how the server starts. You can define it either:
- With
+server.js=>+serverEntry.jswraps your+server.jsin production. - Without
+server.js=>+serverEntry.jsdefines 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()orDeno.serve(), see Example: Bun - Use WebSockets with any library (e.g. Socket.IO), see Example: WebSockets
- Use all CPU cores with Node.js's
clustermodule
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.jsis attached to Vite's development server. Consequently,+serverEntry.jsisn't used in development. In other words:+serverEntry.jsnever 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.mjsWithout +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.jsis responsible for starting the server, whereas with+server.jsthe 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.mjsExample: 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'sfetchNodeHandler(). 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
24678by 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: helloCompared to WebSockets with +server.js:
- You don't need a Vite plugin:
+serverEntry.jsdefines 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
+precompressto generate.br/.gzfiles 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).