Edit

+serverEntry

Environment: server (production)
Global

Define +serverEntry.js to control how your server starts in production: Vike builds it into dist/server/index.mjs, the file you run in production.

// +serverEntry.js
 
// Your +server.js exports, e.g. `{ fetch }`
import server from 'vike:server'
 
// startServer() can be anything, typically a deployment or server tool
startServer({
  port: 3000,
  onRequest: (request) => server.fetch(request)
})
// +serverEntry.ts
 
// Your +server.js exports, e.g. `{ fetch }`
import server from 'vike:server'
 
// startServer() can be anything, typically a deployment or server 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

It gives you full control over your server production entry, for example to:

  • Deploy to a platform without built-in support, e.g. AWS Lambda (see example below)
  • Use another server framework or runtime API such as Bun.serve() and Deno.serve()
  • Use all CPU cores with Node.js's cluster module

Without +serverEntry.js, dist/server/index.mjs is Vike's built-in server entry: a srvx server entry with first-class support for Node.js, Deno, and Bun (and potentially future runtimes). See also: Integration > +server.

Vike uses Universal Deploy under the hood: it provides Vike's built-in server and integrates with deployment providers (e.g. Cloudflare and Vercel).

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 Vike's built-in server.)

You can use it to handle requests before they reach +server.js:

// +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)
  }
})

Example: AWS Lambda

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

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

Native

// +serverEntry.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
 
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
 
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
 
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

You can call Bun.serve() yourself or use srvx (which uses Bun.serve() when running with Bun).

$ vike build
$ bun dist/server/index.mjs

You don't need +serverEntry.js to use Bun: without it, $ bun dist/server/index.mjs runs Vike's built-in server entry powered by srvx which automatically uses Bun.serve() under the hood.

Native

// +serverEntry.js
 
import server from 'vike:server'
 
Bun.serve({
  port: 3000,
  async fetch(request) {
    // 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)
    // Your +server.js exports
    return server.fetch(request)
  }
})
// +serverEntry.ts
 
import server from 'vike:server'
 
Bun.serve({
  port: 3000,
  async fetch(request) {
    // 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)
    // Your +server.js exports
    return server.fetch(request)
  }
})

srvx

// +serverEntry.js
 
import { serve } from 'srvx'
import { staticMiddleware } from 'srvx/static'
import server from 'vike:server'
 
serve({
  port: 3000,
  // Serve the static assets (dist/client/)
  middleware: [staticMiddleware({ dir: `${import.meta.dirname}/../client` })],
  // Your +server.js exports
  fetch: server.fetch
})
// +serverEntry.ts
 
import { serve } from 'srvx'
import { staticMiddleware } from 'srvx/static'
import server from 'vike:server'
 
serve({
  port: 3000,
  // Serve the static assets (dist/client/)
  middleware: [staticMiddleware({ dir: `${import.meta.dirname}/../client` })],
  // Your +server.js exports
  fetch: server.fetch
})

Development

+serverEntry.js is only used in production. In development, $ vike dev uses Vite's development server with your +server.js (as usual, with HMR).

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