# start-server-and-test polls a URL, runs your script, and kills the whole process tree

> start-server-and-test is a small Node utility that starts a server, waits for a URL to respond, runs a test command and shuts the server down, and it ships three names for the same binary. Its interesting edges are the argument-parsing conventions that let you supply a port, a URL, a pipe-separated list or nothing at all.

**bahmutov/start-server-and-test** — Starts server, waits for URL, then runs test command; when the tests end, shuts down server

- Repository: https://github.com/bahmutov/start-server-and-test
- Stars: 1,584 · Forks: 97
- Language: JavaScript
- License: MIT
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/bahmutov-start-server-and-test

## The lifecycle in three steps, and the ps dependency nobody expects

The tool does exactly what its name says. It starts a server process, polls a URL until it responds, runs a test command, and when the test process exits it shuts the server down.

That last part is where a Linux user loses time. The README notes that on Linux, ps must be available, and that minimal Docker images such as node:slim may not include it. On Debian, ps comes from the procps package and may need installing separately. The README points to the tree-kill dependency documentation for the detailed requirements, so the process killing is delegated rather than implemented in this package.

A bare port number works and is the terse form:

```sh
npm install --save-dev start-server-and-test
```

Node version 22 or 24 or above is required, which the package metadata states as the engines field ^22 || >=24. That excludes Node 20 outright, so a project pinned to an older runtime cannot use this without changing its own floor.

Installation is a dev dependency, which is the right place for it. The tool exists to run your test command, and it is not needed at application runtime.

## Argument positions are overloaded, and the README teaches them by example

There is no flag for the server command, the URL or the test command. They are positional, and which position means what depends on how many arguments you pass.

With the npm script convention, where you have a script named start and one named test, you can give just the URL:

```json
{
  "scripts": {
    "start": "npm start",
    "test": "mocha e2e-spec.js",
    "ci": "start-server-and-test http://localhost:8080"
  }
}
```

Shorten the URL to a port and it resolves to http://127.0.0.1:8080. Supply a start command that is not called start, and it becomes the first argument. Supply a test command that is not called test, and it becomes the third. Supply nothing at all and the defaults are npm start and the test script.

The result is a small grammar rather than a set of options, which is compact and occasionally surprising. The practical advice in the README is to specify the full URL rather than the port, and there is a good reason for that beyond convenience: the tool distinguishes localhost, 0.0.0.0 and 127.0.0.1, so a bare port silently picks one of them.

For a v1 to v2 upgrade, the same trap appears again. If you were passing just a port and the resolved localhost:xxxx URL stopped working, the fix is to write the explicit http://localhost:xxxx form.

## Three binary names, one implementation

The package registers three executables in its bin field, all pointing at the same file:

```json
{
  "bin": {
    "start-server-and-test": "src/bin/start.js",
    "server-test": "src/bin/start.js",
    "start-test": "src/bin/start.js"
  }
}
```

Any of them works, and they are interchangeable in scripts. The shorter names matter mostly for line length in package.json.

A colon before a port is also accepted, so server-test :8080 and server-test 8080 do the same thing.

There is a further shorthand where the server command is dropped entirely, leaving the URL to be resolved against the npm start script:

```sh
start-server-and-test start http://127.0.0.1:8080 test
server-test start http://127.0.0.1:8080 test
server-test http://127.0.0.1:8080 test
server-test 127.0.0.1:8080 test
start-test :8080 test
start-test 8080 test
start-test 8080
```

Seven invocations, one behaviour. If you are debugging a CI script that starts behaving differently after an edit, this equivalence list is the place to look, because a dropped word can silently move which argument is the server command.

## Whole commands as strings, and why npm scripts make that work

Arguments do not have to be npm script names. You can pass a complete command, quoted so it stays one argument, and it runs as written:

```sh
# run http-server, then when port 8000 responds run Cypress tests
start-server-and-test "http-server -c-1 --silent" 8000 "./node_modules/.bin/cypress run --record"
```

Inside package.json the quoting gets noisier, and that is where the advice to prefer script names pays off:

```json
{
  "scripts": {
    "ci": "start-server-and-test \"http-server -c-1 --silent\" 8080 \"cypress run --spec cypress/integration/location.spec.js\""
  }
}
```

The README explains why global and local tools can be mixed in that form: npm scripts execute with ./node_modules/.bin on the PATH, so a globally installed http-server and a locally installed cypress both resolve inside an npm script.

It also shows the cleaner refactor, where the server moves into its own start script and the ci script keeps only the test command. The npm start script is then used by default, which is the least surprising arrangement of the three.

## Waiting on several resources, and chaining several test commands

Two composition features are easy to miss.

To wait on more than one resource, pipe the values and quote the whole thing:

```json
{
  "scripts": {
    "ci": "server-test \"8080|http://foo.com\""
  }
}
```

For multiple ports the shorthand is server-test "8000|9000" test. Every listed resource must respond before the tests run, which is how you gate on a database port as well as an application port.

To run several test commands in sequence against one server, separate them with &&:

```json
{
  "scripts": {
    "start": "npm start",
    "test:unit": "mocha test.js",
    "test:e2e": "mocha e2e.js",
    "ci": "start-test 9000 \"npm run test:unit && npm run test:e2e\""
  }
}
```

The README states the failure semantics explicitly: the first command runs after 127.0.0.1:9000 responds, then the second runs when the first finishes, and if either fails the ci script fails. The server shuts down after both, since it is tied to the lifetime of the test command rather than to any one sub-command.

The && also means the second command does not run if the first fails, which is what you want in almost every case but is worth knowing if you were expecting both to report.

## expect, proxy, and knowing when responding is not the same as ready

The wait is a response check, and by default it expects HTTP 200. A server behind authentication will answer with an error code while still being up, and the wait would never complete. The fix is --expect, aliased --expected:

```sh
start-test --expect 403 start :9000 test:e2e
```

The default expected value is 200. This is the option to reach for whenever your app returns 401, 403 or a redirect before it is serving.

Proxies are supported for the resolved URL, through --proxy-host and --proxy-port:

```sh
start-test --proxy-host localhost --proxy-port 8000 start :9000 test:e2e
```

Authentication and protocol are covered too, through --proxy-user, --proxy-password and --proxy-protocol, where the protocol can be socks5 or https. --proxy-pass is accepted as an alias for --proxy-password for backwards compatibility.

What this tool does not do is check application health. A server can return 200 while its database connection is broken, and the tests will start anyway. The README does not document a readiness probe option, so a real one has to come from your own command.

## Where it is the wrong tool

The scope is a local server plus a test command. The README documents no retry budget, no timeout option and no health check, so a service that never comes up means a hung job rather than a failed one unless your CI has its own limit.

It also assumes one server. The pipe syntax waits on several resources but starts only one server, so anything needing two services running means nesting scripts or using a different tool.

For container-based end-to-end testing, an approach that composes services and waits on health checks handles the orchestration natively and does not need ps on the host. For local development against a service you start by hand, this tool is unnecessary overhead.

The upgrade path matters too. The v1 to v2 guidance notes that bare port numbers may stop working as localhost:xxxx, which is a reminder that a tool with implicit argument conventions can change behaviour across majors.

On maintenance: the repository is not archived, the last push was on 2026-10-16, tags v3.0.11, v3.0.12 and v3.0.13 landed in June, August and October 2026, and the package is MIT licensed. The package.json version field reads 0.0.0-development because releases go through semantic-release, so the git tags are the version history rather than the manifest.

## Conclusion

Adopt start-server-and-test if you run end-to-end tests against a locally started service and want one npm script that owns the whole lifecycle, including the shutdown. Do not adopt it if your server needs a readiness signal other than an HTTP response, because the wait is a URL poll and nothing else. Before adding it, confirm ps is present on your Linux CI image, since the tree kill depends on it and slim images often leave it out.

## FAQ

### What Node.js version does start-server-and-test require?

Node.js 22, 24 or above. The package engines field is ^22 || >=24, which excludes Node 20.

### Why does start-server-and-test fail on Linux CI?

The process shutdown relies on ps. On Linux, ps must be available, and minimal images such as node:slim may omit it. On Debian it comes from the procps package, which may need to be installed separately.

### How do I make start-server-and-test wait for more than one port?

Separate the values with a pipe and quote the whole string, for example server-test "8000|9000" test or server-test "8080|http://foo.com". Every listed resource must respond before the tests run.

### What does the --expect option do?

It sets the HTTP status code that counts as a responding server. The default is 200, so a service that answers 403 once it is up needs start-test --expect 403 to finish waiting.

### Can start-server-and-test run more than one test command?

Yes. Separate the commands with && inside the quoted test argument, as in start-test 9000 "npm run test:unit && npm run test:e2e". The second runs only if the first succeeds, and the ci script fails if either fails.

## Sources

- [bahmutov/start-server-and-test on GitHub](https://github.com/bahmutov/start-server-and-test)
- [Issues](https://github.com/bahmutov/start-server-and-test/issues)
- [License: MIT](https://github.com/bahmutov/start-server-and-test/blob/master/LICENSE)
- [README](https://github.com/bahmutov/start-server-and-test/blob/master/README.md)
- [Releases](https://github.com/bahmutov/start-server-and-test/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/bahmutov-start-server-and-test
