Confusion about volumes and live-reloads

I have the following file structure:

my-app
├── docker-compose.dev.yml
├── ui (React)
│   ├── src
│   ├── .dockerignore
│   ├── Dockerfile.dev
│   ├── package.json
│   ├── package-lock.json
├── api (Node)
│   ├── src
│   ├── .dockerignore
│   ├── Dockerfile.dev
│   ├── package.json
│   ├── package-lock.json
├── server (Nginx)
│   ├── Dockerfile.dev
│   ├── nginx.dev.conf

ui/Dockerfile.dev

FROM node:lts

WORKDIR /ui

COPY package.json .
COPY package-lock.json .

RUN npm install

EXPOSE 3000

CMD ["npm", "start"]

api/Dockerfile.dev

FROM node:lts

WORKDIR /api

COPY package.json .
COPY package-lock.json .

RUN npm install

EXPOSE 8080

CMD ["npm", "run", "dev"]

.dockerignore (It’s the same for ui and api)

node_modules
build
.dockerignore
Dockerfile
Dockerfile.prod

docker-compose.dev.yml

version: "3.8"
services:
  ui:
    build:
      context: ./ui
      dockerfile: Dockerfile.dev
    image: my-app-ui
    volumes:
      - ./ui:/ui
      - ./ui/node_modules
    ports:
      - "3000:3000"
    tty: true

  api:
    build:
      context: ./api
      dockerfile: Dockerfile.dev
    image: my-app-api
    env_file:
      - ./api.env
    ports:
      - "8080"

  server:
    build:
      context: ./server
      dockerfile: Dockerfile.dev
    image: my-app-server
    ports:
      - "80:80"
    links:
      - api:api
      - ui:ui

When I run this with docker-compose -f docker-compose.dev.yml up the ui has live-reloads although an empty node_modules folder is generated at my-app/ui/ that gets populated with another .cache folder whenever the development server restarts. The order of the volumes at the ui service is the only way it works and if I don’t add ./ui/node_modules I get an error saying that the package react-scripts was not found. I don’t know why this works the way it does or even at all. As far as I understand it the short syntax for volume declaration uses the colon to separate between the source, which can be either a host path or a name for the volume, and the target, which is the container path where the volume is mounted. This makes me think that the first volume is mounting the contents of my-app/ui/ on the container’s /ui/ path. Shouldn’t this be enough to abilitate live-reloads? Why do I need to add ./ui/node_modules after that for the container to be able to run scripts dependant on packages? I don’t have a node_modules folder on my-app/ui/, I’m installing the dependencies from that Dockerfile. What is it even doing? Is it just specifying a path and letting the Engine create a volume, like the docs suggest?

There’s more: when I declare volumes for the api service like I did with ui:

api:
    build:
      context: ./api
      dockerfile: Dockerfile.dev
    image: snake-api
    env_file:
      - ./api.env
    volumes:
      - ./api:/api
      - ./api/node_modules
    ports:
      - "8080"

Both containers fail to start and these errors are thrown:

ERROR: for ui Cannot start service ui: OCI runtime create failed: container_linux.go:349: starting container process caused “process_linux.go:449: container init caused “rootfs_linux.go:58: mounting \”/var/lib/docker/volumes/1cbe57d474e441fc67073878f45647a08db5c2b774d9b93c7fa4c583ea4ab229/_data\” to rootfs \"/var/lib/docker/overlay2/0c864fb070cec3c96ed46d16219047aa5875e84fbde77415d4f1dd6590e160dd/merged\" at \"/var/lib/docker/overlay2/0c864fb070cec3c96ed46d16219047aa5875e84fbde77415d4f1dd6590e160dd/merged/ui/node_modules\" caused \“mkdir /var/lib/docker/overlay2/0c864fb070cec3c96ed46d16219047aa5875e84fbde77415d4f1dd6590e160dd/merged/ui/node_modules: file exists\”"": unknown

ERROR: for api Cannot start service api: OCI runtime create failed: container_linux.go:349: starting container process caused “process_linux.go:449: container init caused “rootfs_linux.go:58: mounting \”/var/lib/docker/volumes/9257238f321f1fc9705e3a194f99c8bd15846978645548ec7d887dd8c90f90d7/_data\” to rootfs \"/var/lib/docker/overlay2/4f48404c5f6134d8e3592576ccccd2404306b1ac1dada65691686950049beb06/merged\" at \"/var/lib/docker/overlay2/4f48404c5f6134d8e3592576ccccd2404306b1ac1dada65691686950049beb06/merged/api/node_modules\" caused \“mkdir /var/lib/docker/overlay2/4f48404c5f6134d8e3592576ccccd2404306b1ac1dada65691686950049beb06/merged/api/node_modules:
file exists\”"": unknown

UNLESS I first create an empty node_modules folder on each directory (my-app/ui/ and my-app/api/) or, at least, on the api. Doing this makes both services start and allows live-reloading. Forgetting to create that folder on the ui causes it to crash and throw the error above but forgetting to create it on the api will make it print that nodemon was not found, an error similar to the one I experience on the ui when I don’t add ./ui/node_modules. What is happening?