Guides: Layer a backend: Write the layers of a backend as a table, keep the database driver in one layer, give the domain files kinds, and stop import cycles. # Layer a backend > Write the layers of a backend as a table, keep the database driver in one layer, give the domain files kinds, and stop import cycles. This guide writes the architecture of a small backend as tests. It uses two tables and one rule. ## Before you start * Read [Getting started](/archangel/start/getting-started/). * You need a backend whose code is in folders by layer. The example is a users API. A route calls a service. A service uses the domain and the database code. Only the `db` folder talks to PostgreSQL through the package `pg`. * package.json * src/ * routes/ * health.ts * users.ts * services/ * sign-up.ts * db/ * users.ts * domain/ * email.value-object.ts * user-created.event.ts * user.entity.ts This is the config. The steps below tell you about each part of it: archangel.config.ts ```ts import { defineConfig } from '@enhance-eng/archangel'; export default defineConfig({ parts: { routes: 'src/routes/**', services: 'src/services/**', db: 'src/db/**', domain: 'src/domain/**', }, externals: { driver: ['pg'], }, rules: ({ mayImport, rule }) => [ mayImport('backend', { routes: ['services'], services: ['domain', 'db'], db: ['domain', 'driver'], domain: [], }, { because: 'A route calls a service. Only db talks to the database. The domain depends on nothing.', }), mayImport('model', { entity: ['value-object'], event: ['entity', 'value-object'], 'value-object': [], }, { files: 'domain', kinds: 'src/domain/*.{kind}.ts', because: 'An entity does not know its events. A service publishes them.', }), rule('no-cycles', { files: 'src/**', noCycles: true, because: 'Files that import each other cannot change one at a time.', examples: { rejected: ['src/services/a.ts imports src/services/b.ts imports src/services/a.ts'], }, }), ], }); ``` ## Write the layers 1. Give a name to each layer in `parts`. Each file is in one part at most. 2. Give a name to the database driver in `externals`. An externals set is a named set of outside packages. Here, `driver` holds `pg`. 3. Write the table `backend`. It has one row for each layer: * `routes` may import `services`. * `services` may import `domain` and `db`. * `db` may import `domain` and the externals set `driver`. * `domain` may import only itself. A row always permits its own part. The table refuses each other import into the repository. A table refuses a package only when a cell names it The cell `driver` on the row `db` tells the table about `pg`. So the table judges each import of `pg`, and refuses it in each row that does not list `driver`. A table does not judge a package that no cell names. Without the cell, each row could import `pg`. ## Give the domain files kinds The domain has three kinds of file: entities, value objects and events. The file name gives the kind, for example `user.entity.ts`. A second table, `model`, uses the kinds as its rows: * `kinds: 'src/domain/*.{kind}.ts'` is the pattern that gives a file its kind. In `src/domain/user.entity.ts`, the capture `{kind}` takes `entity`. * `files: 'domain'` makes the table judge only the files of the part `domain`. * Because the table has `files`, each file in `domain` must have a row. A file such as `src/domain/helpers.ts` has no kind, so the rule `model.every-file-has-a-kind` refuses it. You do not declare a kind. The pattern gives the names. Read [Kinds](/archangel/concepts/kinds/) for more patterns. ## Stop import cycles The rule `no-cycles` refuses files that import each other. It counts only runtime imports by default, because the compiler removes an `import type`. In the example, `user.entity.ts` imports `user-created.event.ts`, and the event imports the type `User` from the entity. This is a cycle of one runtime import and one type import. It cannot break when the code runs, so `no-cycles` does not refuse it. To refuse type cycles too, add `imports: 'all'` to the rule. The rule has a rejected example. `archangel test` makes the two files of the example in memory and checks that the rule refuses them. ## Check the backend 1. Run the check: Terminal ```ansi $ npx archangel check error[may-import]: entity may not import event, through "./user-created.event": entity may import only value-object, and itself --> src/domain/user.entity.ts:2:29 | 2 | import { userCreated } from './user-created.event'; | ^^^^^^^^^^^^^^^^^^^^^^ entity may import only value-object, and itself | = help: remove the import, or move the code to a file whose row permits the import = note: rule: model.entity = note: because: An entity does not know its events. A service publishes them. error[may-import]: routes may not import the package `pg`, through "pg": routes may import only services, and itself --> src/routes/health.ts:1:22 | 1 | import { Pool } from 'pg'; | ^^^^ routes may import only services, and itself | = help: remove the import, or move the code to a file whose row permits the import = note: rule: backend.routes = note: because: A route calls a service. Only db talks to the database. The domain depends on nothing. checked 8 files and 10 imports: 2 problems For what a code means, run `archangel docs may-import`. ``` The check finds two problems: * `user.entity.ts` imports an event. The row `entity` lists only `value-object`. To fix it, let the service make the event after it creates the user. * `health.ts` imports `pg` in a route. To fix it, move the query to a function in `db`, and call it through a service. 2. Run the examples: Terminal ```ansi $ npx archangel test pass no-cycles rejected #1 src/services/a.ts, src/services/b.ts 1 examples, none failed; 0 rules without a rejected example 7 table rows need no written example; `archangel rules` shows what each covers ``` 3. See what each row judged: Terminal ```ansi $ npx archangel rules backend 4 rows archangel.config.ts:14 because: A route calls a service. Only db talks to the database. The domain depends on nothing. backend.routes: routes may import only services, and itself 2 files · 2 imports judged · 1 problem · 0 exceptions backend.services: services may import only domain, db, and itself 1 file · 2 imports judged · 0 problems · 0 exceptions backend.db: db may import only domain, driver, and itself 1 file · 2 imports judged · 0 problems · 0 exceptions backend.domain: domain may import only itself 3 files · 3 imports judged · 0 problems · 0 exceptions backend.every-file-has-a-kind: a file in no row is not checked here; add `files` to check it 7 files · 0 problems model 3 rows archangel.config.ts:23 because: An entity does not know its events. A service publishes them. model.entity: entity may import only value-object, and itself 1 file · 2 imports judged · 1 problem · 0 exceptions model.event: event may import only entity, value-object, and itself 1 file · 1 import judged · 0 problems · 0 exceptions model.value-object: value-object may import only itself 1 file · 0 imports judged · 0 problems · 0 exceptions model.every-file-has-a-kind: every file of model has a row 3 files · 0 problems no-cycles archangel.config.ts:33 because: Files that import each other cannot change one at a time. files in src/** may not import each other in a cycle 7 files · 0 problems Not judged by any rule: 0 files, 0 imports between repository files. Outside packages that no rule names: none. ``` The table `backend` has no `files`, so its rule `backend.every-file-has-a-kind` does not check files that have no row. The line says this. The table `model` has `files`, so each file of the domain must have a row. ## A table or `layers` The shorthand `layers(['routes', 'services', 'db'])` also writes layers. It says only that a layer may not import a layer before it in the list. So a route may import `db` directly. A table says what each layer may import, so it refuses that import. Use a table when you want to say what each layer may use, not only what it may not use. ## Next steps * [Tables, rows and cells](/archangel/concepts/tables-rows-and-cells/) explains each cell type. * [Exceptions and baselines](/archangel/guides/exceptions-and-baselines/) tells you what to do with code that breaks a rule on purpose. * [Monorepos and workspace tables](/archangel/guides/monorepos/).