TT Lab
Get started
Learn Learning paths Courses

CBA — Backstage Associate

Why the Repository Structure Is Your Customisation Capability

Continue in TT Lab

In one line

What npx @backstage/create-app gives you is not a copy of Backstage but a monorepo that you own. Customizing is not flipping one line of configuration; it is changing the code of that repository, building it, and deploying it, so knowing what is where is itself the ability to customize.

Why this was needed

The questions a team that adopts a portal first runs into are usually these.

These questions have something in common. All of them have answers if you know the repository structure, and none if you do not. Backstage is not a "product you turn on and off with configuration," so there is no answer you can find on screen.

How it works

The repository is one workspace

The tree that create-app makes is a monorepo that uses yarn workspaces.

package.json                루트. private: true, workspaces 에 packages/* 와 plugins/*
packages/app/               프런트엔드 애플리케이션 (React)
packages/backend/           백엔드 애플리케이션 (Node.js)
plugins/<이름>/             사내에서 만든 플러그인
app-config.yaml             설정

The root is private: true because this package itself is never published. And you need to put plugins/* in workspaces so that an app can pull in an internal plugin by a name like @internal/plugin-oncall without publishing it to an npm registry.

backstage.role in package.json is the key

Every package's package.json has a backstage.role. backstage-cli looks at this one value to decide how to build and test that package.

role What it is
frontend The frontend app, of which packages/app is the only one
backend The backend app, of which packages/backend is the only one
frontend-plugin A plugin that runs in the browser
backend-plugin A plugin that runs on the server
common-library · node-library · web-library Shared code that is not a plugin

If you write the role wrong, the build succeeds but the output comes out strange. That is also why a backend package's main must point to the build output (dist/...).

The new backend system only registers

packages/backend/src/index.ts is the whole backend.

const backend = createBackend();
backend.add(import('@backstage/plugin-catalog-backend'));
backend.add(import('@backstage/plugin-catalog-backend-module-github'));
backend.start();

The old backend required building a router by hand for each plugin and passing the logger, configuration, and database yourself. In the new backend system, you only register and the required things are injected. Here the difference between a plugin and a module comes up on the exam.

If the name contains -module-, it does not work alone, and the plugin it pairs with must be registered together.

A frontend plugin starts from three files

src/routes.ts   createRouteRef 로 라우트 참조를 만들어 내보낸다
src/plugin.ts   createPlugin 으로 플러그인을 만들고, 페이지를 확장으로 제공한다
src/index.ts    바깥에 공개할 것만 다시 내보낸다

There is a reason to put the route reference in a separate file. If a page component references the plugin and the plugin references the component again, a circular import arises. If you keep the file that holds only the reference separate, that loop is broken.

And a page that has a route is made with createRoutableExtension, and you hook the route reference to mountPoint. The component is passed as a lazy import, so the code is not downloaded until that plugin is actually opened.

Keeping only index.ts as the public entry point is discipline too. Once the app starts importing a plugin's internal files directly, you can never change the plugin's internal structure again.

To attach a tab to an entity page, you change two places

<EntitySwitch>
  <EntitySwitch.Case if={isKind('component')}>
    <EntityLayout>
      <EntityLayout.Route path="/oncall" title="On-call">
        <OncallPage />
      </EntityLayout.Route>

You show different pages per kind with EntitySwitch and isKind, and attach a tab with EntityLayout.Route. A common mistake comes from here. If you attach only the tab and do not register the route in App.tsx, the tab shows but a blank screen appears when you click it. There is no error on screen, so the cause is hard to find.

Development workflow

yarn install --immutable means "do not modify the lock file." If you drop this flag in CI, dependencies quietly go up, and a commit that passed yesterday builds differently today. Next come type checking, building, and testing in that order. This order matters because a single type error hides all the other output.

What it looks like in the field

This repository has the same boundary. logos.js, i18n.js, and backend/app/grader_paths.py are generated files, so if you edit them by hand they vanish at the next generation. What you should fix is the generator side. A Backstage app has a boundary of exactly the same nature. packages/app/src is my code, and the plugins inside node_modules are other people's code. If you blur the boundary and start editing other people's code directly, upgrading becomes impossible.

One more. On a day when several people were editing this repository in parallel, there was a time when a single TypeScript syntax error made the checker finish without looking at the rest at all. "The checker found nothing" was actually "nothing was checked." A portal repository has the same thing happen once it grows. This is why you put type checking before the build in CI.

What you will do in the next lab

In /root/cba-app/, you set up the repository structure that create-app would produce. You create the workspace root, two apps, and two internal plugins, and give each package a role. Then you register a plugin and a module the new backend system way, write the three files of a frontend plugin, and attach a tab to an entity page. Finally you extract a package inventory and set up a CI gate. This is an environment where node dependencies cannot be downloaded, so you do not build; you work only with the structure and the wiring.