Logo VitNode

CLI

Build and start for production

Build a VitNode app, API app or plugin with vitnode build, then run the production server with vitnode start.

vitnode build runs your project's real production build and reports the size of every file it wrote. vitnode start then runs the server that build produced, and tells you once it accepts connections.

vitnode build
vitnode start

Build an app

vitnode build (shortened)
◆ VitNode
  Production build

  ✓ Configuration loaded, plugin routes generated  1.5s
  ✓ Building client  21.7s
  ✓ Building server  12.8s
  ✓ Packaging server (Nitro)  21.9s
  ✓ Measuring output  1.1s

Output

Client JS  .output/public
  File                              Size       gzip
  ─────────────────────────────────────────────────
  ▲ assets/custom-emoji-BPTfd_f5.js   820.6 kB   163.2 kB
  ! assets/editor-CUh_DSIT.js         319.4 kB   101.0 kB
  ✓ assets/search-DxtCMbN7.js         223.9 kB    69.1 kB
  … 4721 more not shown (--verbose lists all) - 4731 files, 18.1 MB, gzip 4.5 MB

Server  .output/server
  File          Size
  ──────────────────
  ✓ index.mjs   1.2 MB

  ✓ Built in 1m 6s
  Run pnpm start to serve it.

The last line names your app's start script, run with the package manager you use - pnpm start, bun start, yarn start or npm start. An app without a start script is told to run vitnode start instead.

The steps are the build environments your app really has. For a TanStack Start app with Nitro, that is the client, the server, then Nitro's packaged server. VitNode's Vite plugin generates plugin routes and registries while the configuration loads.

The symbols mark how a file compares with its size budget: ✓ is fine, ! is worth a look and ▲ is large. From the second build on, you also see what grew or shrank since the last one. Bundle size report explains the thresholds, the comparison and --analyze.

See which pages are static

The build ends with every route your app serves - your own route files and the pages of core and every plugin - marked by how each one is served:

vitnode build (end of the output, shortened)
Routes
  ├ ○ /
  │   ├ /
  │   └ /pl
  ├ ƒ /admin/*  24 AdminCP routes (--verbose lists all)
  ├ ƒ /login
  ├ ● /solutions/:slug
  │   ├ /solutions/help-center
  │   ├ /pl/solutions/help-center
  │   └ … 8 more paths
  └ ƒ /users/:nameCode

  ○  Static    prerendered as static HTML
  ●  Partial   some paths prerendered, the rest rendered on demand
  ƒ  Dynamic   rendered on demand

A route is static when every language has a prerendered file, and partial when some of its paths do - a route with a parameter, such as /solutions/:slug, is always partial once any of its values is prerendered. The paths under a route are the files the build wrote, each page followed by its translations. The AdminCP is never prerendered, so its routes share one line unless you pass --verbose.

Nothing is static until you ask for it. Prerender static pages shows how.

Build an API app or a plugin

The same command builds other project types:

ProjectWhat vitnode build does
API app (Node)Compiles with tsc and tsc-alias into dist, then lists the output
API app (Bun)Nothing: Bun runs src/index.ts directly
Plugin packageCompiles src into dist/src with tsdown and checks types with tsc --noEmit
vitnode build in a plugin package
◆ VitNode
  Building @acme/blog

  ✓ API registry generated
  ✓ Compiling sources  8.0s
  ✓ Checking types  7.3s

  ✓ Built in 8.0s

Compiling and type checking run at the same time, and both must pass. Build and watch a plugin explains what ends up in dist.

When a build fails

✖ Build failed
  Plugin   @vitnode/blog
  File     plugins/blog/src/pages/post.tsx:42:9
  Step     vite:oxc

  Missing export: title

Plugin appears when the failing file belongs to a VitNode plugin package, and File and Step when the bundler reported them. Below them you get the bundler's message and a code frame, then the last 60 lines of build output. Run with --verbose for the full output and stack trace.

Start the production server

vitnode start
◆ VitNode
  Production

  ● Running  http://localhost:3000

vitnode start is not a second server. It runs your build's own entry, exactly as node <entry> would, and adds two things: "Running" appears only once the server accepts connections, and Ctrl+C gives the server time to close cleanly.

ProjectEntry it runsDefault port
AppNitro's server entry, usually .output/server/index.mjs, on Node3000
API app (Node)dist/index.js on Node8000
API app (Bun)src/index.ts on Bun8000

--port and --host set the PORT and HOST variables your server already reads. The port falls back to PORT, then the default above. NODE_ENV is set to production unless you set it yourself.

vitnode start stops with an error when:

  • there is no build yet: run vitnode build first
  • the app was built for a hosting platform, such as the vercel Nitro preset, because that platform runs it
  • something already listens on the port
  • the runtime is missing, for example Bun on a server that only has Node
  • the server exits before it is ready, or does not listen within 60 seconds

Next steps