Direct answer
For a legacy repository with little to no automated tests, the safest way to introduce testing is to start with unit tests on the most stable, highest-value logic and consumer-driven contract tests at service boundaries, since both let teams refactor with real safety without requiring the risky, expensive step of building full end-to-end coverage first.
Structured elaboration
Why this order: unit tests are the cheapest to write and give the fastest feedback on core logic correctness, so they are the natural starting point for any codebase. Consumer-driven contract tests come next specifically because they protect the riskiest part of introducing tests into a legacy system, the interfaces between components or services, without requiring a full integration environment; a contract test catches a breaking change to how one part of the system calls another well before a slow, expensive end-to-end test would.
Safe first targets: pick the modules or services that are both frequently changed (so tests pay off quickly by catching regressions on every future change) and relatively well-understood (so writing a correct test does not itself require reverse-engineering undocumented behavior first). Avoid starting with the most tangled, least-understood part of the legacy system, even if it feels like the highest-risk area, since writing a wrong test there does more harm (false confidence) than writing no test at all.
CI integration: wire the new tests into the build pipeline from day one, even if coverage starts small, so the habit of running tests on every change is established early rather than retrofitted later once dozens of untested changes have already been merged without a safety net.
Success metrics to track: the number of previously-untested modules that now have baseline coverage, the number of contract tests protecting service boundaries, and, critically, the number of refactors or changes that were caught being unsafe by a new test before reaching production, which is the actual proof this effort is paying off, not raw test count alone.
Worked example
Concretely, for a legacy repository with a dozen loosely-coupled modules: phase 1 (first month), identify the two most frequently-changed, best-understood modules and add unit test coverage for their core logic, plus consumer-driven contract tests for the two service boundaries those modules expose to the rest of the system. Phase 2 (months 2-3), expand to the next tier of frequently-changed modules, and begin using the contract tests as a real safety net for a planned refactor of one of the covered boundaries, demonstrating the approach's value concretely (the refactor ships with confidence because the contract test would have caught an accidental breaking change). Track success as: 2 modules covered by month 1, 5 by month 3, and at least one real instance where a contract test caught a genuine breaking change before it reached production, which becomes the concrete evidence used to justify continuing the investment.
Trade-offs and pitfalls
The most common mistake is starting with the most complex, highest-risk-looking part of the legacy system out of a sense of urgency, when in practice a wrong or superficial test there provides false confidence and can be worse than having no test. The other mistake is delaying CI integration until "enough" tests exist, which misses the compounding value of catching regressions from day one, however small the initial coverage.