TT Lab
Get started
Learn Learning paths Courses

CBA — Backstage Associate

Source Changed, but the Local Backend Still Answers the Same

Continue in TT Lab

Goal

You set up yourself a Yarn 4 workspace shaped like a Backstage app repository, and go around the whole development flow once: dependency installation, the lock file, TypeScript compilation, build outputs, and running locally. The grader judges not by looking only at files but by the result of running yarn, tsc, and node again.

Why it matters

"It works on my PC but installation fails in CI," "a type error occurred, so why was dist created?", "I changed the source but the response is the same" — what you run into most often when developing Backstage is this flow rather than plugin code. You find the cause quickly only if you know what the lock file pins, what tsc catches and what it misses, and which file the backend actually runs.

The first VM start takes about 4 minutes, and the installation in step 2 takes tens of seconds from the internet. The real project's backstage-cli (yarn start, yarn build) and frontend app (packages/app) have a large installation size and long build time, so they are not used, and you do the same work by hand with yarn, tsc, and node. This VM has no docker, so you do not build an image.

Steps

  1. Set up the workspace with three package.json files (root, backend, plugin) and .yarnrc.yml.
  2. Create the lock file and the workspace links with yarn install.
  3. Record that a package.json that diverges from the lock file is blocked by --immutable, and revert it.
  4. Record that tsc catches a wrong policy value, and that JS is still produced anyway.
  5. Turn on noEmitOnError, fix the error, and build.
  6. Bring up the backend with the plugin attached locally.
  7. Confirm that if you only edit the source and restart, nothing changes, and it changes only after you build.
  8. Write the development flow checklist with values measured on the current repository.

Notes

Set up a workspace shaped like the app repository

Create a Yarn workspace in /usr/local/cba-dev. The root package.json has private: true, packageManager: "yarn@4.9.2", and workspaces: ["packages/*", "plugins/*"]. The root .yarnrc.yml has nodeLinker: node-modules, enableTelemetry: false, enableGlobalCache: false, enableMirror: false, and globalFolder: /usr/local/cba-dev/.yarn-global. The dependencies of packages/backend/package.json (name backend) are @backstage/backend-defaults 0.17.8, better-sqlite3 12.4.1, and @internal/plugin-hello-backend workspace:^. The dependencies of plugins/hello-backend/package.json (name @internal/plugin-hello-backend, main dist/index.js, types dist/index.d.ts, scripts build: tsc -p tsconfig.json) are @backstage/backend-plugin-api 1.10.0 and express 4.22.3, and its devDependencies are typescript 5.9.3 and @types/express 4.17.25. Write all external versions without range symbols. corepack yarn workspaces list must show three workspaces.

A Backstage app is a Yarn workspace split into packages/app (the frontend), packages/backend, and plugins/*. This VM does not bake the app bundle, so you have only the backend and the backend plugin. corepack looks at the packageManager field and downloads and uses that version of yarn. The root disk is small, so first run in the shell export COREPACK_ENABLE_DOWNLOAD_PROMPT=0 COREPACK_HOME=/usr/local/cba-dev/.corepack TMPDIR=/usr/local/cba-dev/.tmp (and mkdir -p /usr/local/cba-dev/.tmp). If you do not turn off the global cache and mirror, copying into the home directory runs out of space.

One installation creates the lock file and the workspace links

Run corepack yarn install in /usr/local/cba-dev. yarn.lock must be created at the root, node_modules/@internal/plugin-hello-backend must be a link that points to plugins/hello-backend, and node_modules/@backstage/backend-defaults must be 0.17.8. Then corepack yarn install --immutable must also succeed.

A Yarn workspace gathers dependencies into the root node_modules, and dependencies on the workspace: protocol are linked rather than copied. Check with ls -l node_modules/@internal and grep -n 'backend-defaults@npm' yarn.lock. A peer dependency warning (YN0086) is not an installation failure.

A package.json that diverges from the lock file is blocked in CI

Change only the express version in plugins/hello-backend/package.json to 4.21.2 and, leaving yarn.lock as it is, run corepack yarn install --immutable and save its entire output to /root/cba-dev/immutable.txt (also look at the exit code). Then revert express to 4.22.3 so that --immutable succeeds again. yarn.lock must not change to the end.

--immutable fails instead of writing if the installation result would require changing the lock file. The reason to use this option in CI is to keep dependency versions that were resolved only on a developer PC from sneaking into a deployment. After reverting, do not run install without the option — that would record the diverged version in the lock file. The failure code comes out as a number starting with YN.

tsc catches a wrong policy value but still produces JS

Create plugins/hello-backend/tsconfig.json (target ES2022, module commonjs, moduleResolution node, strict, esModuleInterop, skipLibCheck, declaration, rootDir src, outDir dist, include ["src"], without noEmitOnError) and src/index.ts. index.ts creates a plugin with pluginId hello using createBackendPlugin so that GET /ping returns {"version": 1}, and deliberately writes http.addAuthPolicy({ path: '/ping', allow: 'public' }). Export helloPlugin both by name and as default. After deleting dist, run corepack yarn tsc -p tsconfig.json in the plugin directory, save the output to /root/cba-dev/tsc-error.txt, and append at the end one line emitted_despite_error=<dist/index.js 가 생겼으면 yes, 아니면 no> (the placeholder is yes if dist/index.js was created, otherwise no).

The allow of addAuthPolicy is not just any string but a fixed literal union type. The reason to use TypeScript is to catch at the compile stage a mistake that you would only learn about after starting up in JavaScript. However, tsc's default is to write output even when there are type errors — look at the exit code and dist together.

Build so that nothing is emitted if there is a type error

Put "noEmitOnError": true in tsconfig, fix the policy value in index.ts to the correct 'unauthenticated', and build with corepack yarn workspace @internal/plugin-hello-backend build. dist/index.js and dist/index.d.ts must be produced from the current src, and corepack yarn tsc -p tsconfig.json --noEmit must finish with no errors in the plugin directory.

The build output (dist) must be newer than the source. yarn workspace <이름> <스크립트> runs a specific package's script from the root (the placeholders are the workspace name and the script name). If you turn noEmitOnError on and deliberately get it wrong, you can also confirm that dist is not updated.

Bring up locally a backend with the workspace plugin attached

In packages/backend/index.js, add require('@internal/plugin-hello-backend') to createBackend() and start it. Put app-config.yaml (backend.baseUrl http://localhost:7007, listen.port 7007, DB better-sqlite3 and ':memory:') at the workspace root /usr/local/cba-dev/app-config.yaml. Bring it up from packages/backend with node index.js, appending the output to packages/backend/backend.log, and it is fine if GET http://127.0.0.1:7007/api/hello/ping without authentication is 200 {"version":1}.

If you do not give --config, the backend looks for app-config.yaml not in the working directory but at the repository root — see in the first log line what error occurs if you put the configuration in packages/backend. require follows the link in node_modules and goes to the main of the plugin's package.json. Check the real file with node -p "require('fs').realpathSync(require.resolve('@internal/plugin-hello-backend'))".

The response changes only after you edit the source, build, and restart

Change the /ping response in index.ts to {"version": 2}. First, restart only the backend without building and see that the response is still 1, and then build the plugin and restart the backend so that it becomes 2. In /root/cba-dev/dev-loop.txt, leave two lines, without_build=<빌드 없이 재시작했을 때 version> and after_build=<빌드 뒤 재시작했을 때 version> (the placeholders are the version after restarting without a build and the version after restarting following a build).

What the backend runs is not the TypeScript in src but the JavaScript in the dist that main points to. In a real Backstage repository, yarn start (backstage-cli) does this conversion and restart for you, but this lab walks through the steps by hand without that tool.

Fill in the development flow checklist with the current repository

In the first five lines of /root/cba-dev/report.md, write the values measured now — yarn_version= (corepack yarn --version at the root), workspace_count= (the number of lines in workspaces list), immutable_install= (pass if --immutable succeeds now, otherwise fail), plugin_entry= (the real path of the plugin as resolved by require.resolve from packages/backend), and ping_version= (the version of /api/hello/ping now). Below that, write what the lock file, type checking, and build outputs each block in the development flow, and the reason you did not build a Docker image on this VM.

Every value is obtained by running the command now. The real path is the realpath after following symbolic links. Check whether the tool exists with command -v docker.