Why the ExApp frontend toolchain is different
In a PHP Nextcloud app, Webpack outputs to js/ at the app root, and \OCP\Util::addScript() tells Nextcloud to include that file in the page. Nextcloud handles the rest.
ExApps work differently in two important ways:
1. Assets are served by your FastAPI server, not Nextcloud. Your compiled JavaScript, CSS, and images live in ex_app/js/, ex_app/css/, and ex_app/img/. When nc_py_api starts your server, it automatically mounts these folders to FastAPI's static file serving. Nextcloud (via AppAPI's proxy) fetches them from your server.
2. The Webpack public path must point through the AppAPI proxy. When the browser loads your JavaScript and that script tries to fetch chunks or assets, the URLs must go through http://nextcloud.local/apps/app_api/proxy/pinboard_python/js/ — not directly to your FastAPI server's port. Without this, asset loading breaks in production (and in development, because the browser can't reach port 23000 directly).
Everything else — npm, Vue SFCs, @nextcloud/vue, @nextcloud/axios — is identical to the PHP track.