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 startBuild an app
◆ 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:
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 demandA 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:
| Project | What 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 package | Compiles src into dist/src with tsdown and checks types with tsc --noEmit |
◆ VitNode
Building @acme/blog
✓ API registry generated
✓ Compiling sources 8.0s
✓ Checking types 7.3s
✓ Built in 8.0sCompiling 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: titlePlugin 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:3000vitnode 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.
| Project | Entry it runs | Default port |
|---|---|---|
| App | Nitro's server entry, usually .output/server/index.mjs, on Node | 3000 |
| API app (Node) | dist/index.js on Node | 8000 |
| API app (Bun) | src/index.ts on Bun | 8000 |
--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 buildfirst - the app was built for a hosting platform, such as the
vercelNitro 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