servexy
v1.4.0
Published
Fast production-ready static file server with reliable SPA routing.
Maintainers
Readme
Everything a file server should do
Contents
- Requirements
- Install
- Usage
- Recipes
- SPA routing
- Safe by default
- How responses are served
- What servexy is not
- Troubleshooting
- Build from source
- License
Requirements
- macOS, Linux or Windows, on x64 or ARM64.
- For the npm package: Node.js 18 or later, or Bun. The prebuilt binaries need nothing else.
Install
With Bun or npm
bun add --global servexy
# or
npm install --global servexyRun the same commands again to update. To try it without installing:
bunx servexy ./dist --spa
# or
npx servexy ./dist --spaThe package ships a small launcher plus a native binary for every supported platform. The launcher picks the right one, so there is no download step during install and nothing runs at install time.
Or download the binary
Download the file for your system from the latest release:
| System | File | | ---------------------- | --------------------------- | | macOS, Apple silicon |
servexy-darwin-arm64| | macOS, Intel |servexy-darwin-amd64| | Linux, x64 |servexy-linux-amd64| | Linux, ARM64 |servexy-linux-arm64| | Windows, x64 |servexy-windows-amd64.exe| | Windows, ARM64 |servexy-windows-arm64.exe|Rename it to
servexy(servexy.exeon Windows) and move it to a folder on yourPATH. On macOS and Linux, make it executable too:chmod +x servexy sudo mv servexy /usr/local/bin/On macOS, a binary downloaded with a browser is quarantined. Clear the flag once:
xattr -d com.apple.quarantine /usr/local/bin/servexy
Every release includes a SHA256SUMS file. Put it next to the binary and run:
shasum -a 256 --check --ignore-missing SHA256SUMS # macOS
sha256sum --check --ignore-missing SHA256SUMS # LinuxOn Windows, compare the output of Get-FileHash servexy-windows-amd64.exe with the matching line in SHA256SUMS.
npm releases are published from GitHub Actions with provenance, so npmjs.com shows the exact commit and workflow that built each version.
Or install with Go
go install github.com/rahatsagor/servexy@latestThis needs Go 1.27 or later. Builds made this way report their version as dev.
Usage
servexy [OPTIONS] [DIRECTORY]servexy # serve the current folder on http://localhost:8080
servexy ./public --port 3000 # another folder and port
servexy ./dist --spa # a single-page app with client-side routing
servexy ./dist --spa --cache 1h # also let browsers cache assets for an hour
servexy ./public --network # reachable from other devices on your network| Option | Default | What it does |
| ----------------------- | -------------- | ----------------------------------------------------------------------------- |
| DIRECTORY | current folder | The folder to serve. Give it here or with --dir, not both. |
| -p, --port <port> | 8080 | Port to listen on, from 1 to 65535. |
| -d, --dir <directory> | current folder | The folder to serve. |
| -s, --spa | off | Serve the root index.html for page navigations that don't match a file. |
| -n, --network | off | Listen on every network interface instead of only 127.0.0.1. |
| --cache <duration> | off | Let browsers cache files other than HTML, for example 10m, 1h or 24h. |
| --no-compress | off | Turn off gzip compression. |
| --dotfiles | off | Serve hidden files and folders such as .env. |
| -v, --version | | Print the version. |
| -h, --help | | Print help. |
- Options can come before or after the folder, and values can be written as
--port 3000or--port=3000. --ends the options, which lets you serve a folder whose name starts with a dash:servexy -- -drafts.- The single-dash forms
-port,-dir,-spaand-networkfrom older versions still work.
Press Ctrl+C to stop. Requests that are still running get up to five seconds to finish; press Ctrl+C again to quit at once.
Recipes
Preview a production build
| Built with | Output folder | Command |
| ---------------------------------- | --------------------- | ------------------------------------- |
| Vite (Vue, React, Svelte, Solid) | dist/ | servexy ./dist --spa |
| Create React App | build/ | servexy ./build --spa |
| Angular 17 or later | dist/<app>/browser/ | servexy ./dist/my-app/browser --spa |
| Next.js with output: 'export' | out/ | servexy ./out |
| Astro (static output) | dist/ | servexy ./dist |
| Hugo | public/ | servexy ./public |
| Eleventy | _site/ | servexy ./_site |
Vue Router's history mode, React Router's createBrowserRouter, and any other router that uses real URLs need --spa so a refresh on a deep link works. Hash routing (/#/settings) works without it.
To pin it to your project, add it as a dev dependency with bun add --dev servexy (or npm install --save-dev servexy) and use it in a script:
{
"scripts": {
"build": "vite build",
"preview": "servexy ./dist --spa"
}
}Test on your phone
servexy ./dist --spa --networkOpen the network: address servexy prints on any device on the same Wi-Fi. If nothing loads, check that your firewall allows incoming connections for servexy. Everyone on that network can reach the folder while it runs, so only share what you mean to.
Run end-to-end tests against the real build
Point Playwright at your build output, so tests exercise exactly what you ship:
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
webServer: {
command: 'bunx servexy ./dist --spa --port 4173',
url: 'http://127.0.0.1:4173',
reuseExistingServer: !process.env.CI,
},
use: { baseURL: 'http://127.0.0.1:4173' },
});Because missing assets and fetch() calls stay 404, a broken import or API path fails the test instead of hiding behind your index.html.
Cache hashed assets
servexy ./dist --spa --cache 24hHTML keeps Cache-Control: no-cache, so a new deploy shows up on the next refresh. Everything else gets public, max-age=86400. Use this when your bundler puts a content hash in file names, like assets/index-3f9a1c.js. Without hashes, browsers may keep an old file until the cache runs out.
SPA routing
Most SPA servers send index.html for every path they can't find. That keeps refreshes working, but it also answers a missing app.js or a mistyped API call with 200 OK and a page of HTML, which turns a clear 404 into a confusing parse error. servexy only sends your app to requests that are actually page loads.
With --spa, each request is answered like this:
- If the path is a file, or a folder with its own
index.html, that is served. - Otherwise, if the request is a page navigation, the root
index.htmlis served with200 OK. - Anything else gets
404 Not Found.
- Modern browsers send Fetch Metadata headers. A request is a navigation when
Sec-Fetch-Modeisnavigate, or whenSec-Fetch-Destisdocument,iframeorframe. When these headers are present, they decide on their own, sofetch(), images, scripts and styles never get the app. - Otherwise, a request whose
Acceptheader includestext/htmlorapplication/xhtml+xmlcounts as a navigation. - Tools like curl send
Accept: */*or noAcceptat all. Those get the app only when the path has no file extension, socurl localhost:8080/settingsreturns your app andcurl localhost:8080/missing.jsreturns404.
That is why dotted routes like /users/jane.doe load in the browser, while /assets/app.jss stays 404. GET and HEAD are treated the same way.
SPA mode needs an index.html at the root of the folder and won't start without one. A page load of a folder that has no index.html of its own also gets the app, not a file listing.
Safe by default
Pass --dotfiles only when you really want hidden files served.
On Windows, path parts that look like 8.3 short names, such as GIT~1, are treated as hidden too, because NTFS can resolve them to .git or .env. As a result, a file whose name contains ~ followed by a digit isn't served on Windows unless you pass --dotfiles.
How responses are served
- Responses are gzipped when the browser accepts gzip, the body is at least 1 KiB, and the content is text: HTML, CSS, JavaScript, JSON, web manifests, WebAssembly, XML or SVG.
- Images, video, fonts and archives are already compressed, so they are sent as they are, using zero-copy
sendfilewhere the system supports it. HEADand range requests are never compressed, and compressed responses don't offer byte ranges.- Every compressible response includes
Vary: Accept-Encoding, so a cache in front of servexy keeps the gzipped and plain versions apart.
| Response | Cache-Control |
| --------------------------------------------------------- | --------------------------------------------------------- |
| HTML, paths without an extension, folders, SPA fallback | no-cache |
| Every other file | no-cache, or public, max-age=<seconds> with --cache |
| Errors such as 404 | none |
Files are sent with Last-Modified, so browsers revalidate with a quick 304 Not Modified instead of downloading the file again.
- Request headers must arrive within 5 seconds and are limited to 1 MiB. Idle keep-alive connections close after 60 seconds.
- There is no overall write timeout, so large downloads on slow connections are never cut off.
- Without
--spa, a folder without anindex.htmlshows a simple file listing, with hidden files left out. - On Ctrl+C or
SIGTERM, servexy stops accepting connections and waits up to 5 seconds for running requests before closing them. A second Ctrl+C exits at once.
What servexy is not
- A dev server. It serves files that already exist. There's no bundling and no hot reload; keep using
vite,next devand friends while you write code, and use servexy to check the build. - An HTTPS server. It speaks plain HTTP. For HTTPS, put it behind a reverse proxy such as Caddy, or use a tunnel.
- A full web server. There are no request logs, rewrites, headers config or virtual hosts. If you need those, nginx or Caddy are the right tools.
Troubleshooting
Another program is listening on that port, often another dev server. Stop it, or pick another port with --port 3000.
Most systems only let administrators use ports below 1024. Use a higher port, or run servexy behind a reverse proxy. Windows also reserves some port ranges; list them with:
netsh interface ipv4 show excludedportrange protocol=tcp--spa needs to point at your build output, such as dist/ or build/, not the project folder. Run your build first.
Add --spa. Without it, servexy only serves files that exist, so /dashboard/settings is a 404.
- Make sure you passed
--networkand both devices are on the same network. Guest networks and some office or hotel Wi-Fi stop devices from reaching each other. - Use the
network:address servexy prints, notlocalhost. - Allow incoming connections for servexy in your firewall. If the macOS firewall is on, it asks the first time; on Windows, allow it for private networks.
- Check whether its path has a hidden part, such as
.cache/or.vitepress/. Pass--dotfilesif you really mean to serve it. - Check whether it's a symlink that points outside the folder you're serving.
- On Windows, check whether a part of the path looks like
NAME~1.
The package manager skipped some of the package's files. Reinstall servexy. If you run it from a clone of this repository, build the binaries first with bun run build:binaries.
Still stuck? Open an issue with your operating system, the command you ran and what servexy printed.
Build from source
You need Go 1.27.2 or later, Bun 1.4.2 or later, and Node.js 18 or later for the launcher tests.
git clone https://github.com/rahatsagor/servexy.git
cd servexy
go run . ./some-folder --spa # run from source| Command | What it does |
| ------------------------ | -------------------------------------------------------------- |
| bun run check | Go tests and go vet. |
| bun run test:race | Tests with the race detector. |
| bun run vuln | Scan for known vulnerabilities with govulncheck. |
| bun run build:current | Build the binary for this machine into dist/. |
| bun run verify:current | Check that binary runs and reports the package version. |
| bun run test:launcher | Test the npm launcher, including Ctrl+C handling. |
| bun run build:binaries | Build all six release binaries and dist/SHA256SUMS. |
| Path | What's inside |
| -------------------- | ---------------------------------------------------------------------- |
| main.go | The whole server: options, file system, SPA routing and compression. |
| main_test.go | Tests for routing, compression, caching, hidden files and options. |
| bin/servexy | The npm launcher that starts the right native binary. |
| scripts/ | Build, package verification and launcher test scripts. |
| docs/media/ | Images used in this README. |
| .github/workflows/ | CI on Linux, macOS and Windows, and the release workflow. |
Releases are built and published by GitHub Actions when a version tag is pushed. npm trusted publishing is set up for the rahatsagor/servexy repository and the publish.yml workflow, so no npm token is stored anywhere.
Set the new
versioninpackage.jsonand commit it.Tag the commit and push both:
git tag vX.Y.Z git push origin main vX.Y.Z
The workflow tests, vets and scans the code, builds all six binaries, checks their checksums and that the tag matches package.json, creates the GitHub release, and publishes to npm with provenance. Every workflow action is pinned to a commit SHA.
Bug reports and ideas are welcome as issues.
License
Free and open source under the MIT License. © 2026 Rahat Sagor.
