You start a dev server and get EADDRINUSE. The usual response is to kill whatever is on the port and move on, which works right up until the process you killed was something you needed.
Finding the owner
The standard tool is lsof, which lists open files including sockets:
lsof -nP -iTCP:3000 -sTCP:LISTEN
The -n and -P flags skip DNS and service-name lookups, which is the difference between an instant answer and a multi-second hang when a nameserver is slow. On newer macOS versions lsof can also be slow across all processes, so scoping it to the port matters.
For a broader view, netstat still works:
netstat -anv -p tcp | grep LISTEN
Check before you kill
The PID alone does not tell you enough. Three things are worth knowing first: the full binary path, since a process named node could be any of a dozen projects; the parent process, since killing a child that a supervisor restarts accomplishes nothing; and whether it is a launchd job, in which case launchd will bring it straight back and you need to unload it instead.
ps -o pid,ppid,user,command -p <PID>
The common surprises on a developer Mac: a Docker container publishing the port, a previous dev server orphaned when a terminal closed, macOS AirPlay Receiver holding 5000 and 7000, and a stale watch process from a crashed test runner.
Seeing it without the terminal
Burrow's Ports tool lists every listening port with the process behind it, the binary path, and the owning user, refreshed live. You can see that port 3000 belongs to a node process from a project you closed last week, confirm the path, and stop it from the same window. Its Network tool covers the related question of which process is actually moving traffic, per interface and per process.