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 how each page is served
The build ends with every page route your app serves - your own route files and the pages of core and every plugin - with how it is rendered and whether its HTML is reused:
Routes
Route Rendering Page cache
────────────────────────────────────────────────────────────────────────────────────────────────────
/ Static Build-time HTML
├ / prerendered
└ /pl prerendered
/admin/* 24 AdminCP routes (--verbose lists all) SSR None
/blog/article/:slug SSR TTL 5m · SWR 1m
└ /pl/wpisy/:slug pattern
/login SSR None
/solutions/:slug Partial Build-time HTML × 10, others: None
├ /solutions/help-center prerendered
└ … 9 more URLs
Static HTML written at build time; a route with parameters lists the URLs it generated
SSR, cached rendered on request, then reused for every visitor (TTL, then SWR window)
SSR, uncached rendered on request for each visitor
Page cache storage: memory - one copy per server process, emptied on restart.- Rendering is
Staticwhen every language has a prerendered file,Partialwhen only some URLs of the route do, andSSRotherwise. A route with a parameter, such as/solutions/:slug, is partial once any of its values is prerendered. Its row shows the pattern, and theprerenderedlines under it are the concrete URLs the build wrote, each page followed by its translations. - Page cache shows the page cache policy the server applies after your app's overrides:
TTL 5m · SWR 1m, orNone.(app)marks a policy set invitNodeConfig.pageCache.routes, andNone (disabled by app)a plugin policy your app turned off. Thepatternlines are the same route in your other languages and domains, which the cache keys separately. - In a color terminal, static output is green, cached SSR is blue and uncached SSR keeps the normal text color.
The last line names the storage when the build can tell it. Redis is reported as runtime-dependent, because the server connects to REDIS_URL only when it starts. If routes declare a policy but src/server.ts does not use withPageCache, the build warns that nothing is cached. The report describes configuration, not how often the cache is hit.
The AdminCP is never prerendered or cached, 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