The situation
The platform sits between vendors and large employers: vendors list what they offer, companies pass it on to their staff as benefits. There’s a mobile app on top of it, which doubles as the channel a company uses to reach its own people. All of it had been quietly running on PHP 8.0 and Laravel 8. It worked. That was the problem — nothing was obviously broken, so nobody had a reason to touch it, and the gap to a supported version kept growing.
There were no tests. Not a thin suite, not an outdated one — none. Every deploy was somebody clicking through the app afterwards and hoping.
The API had the same history. Instead of CRUD there were dozens of endpoints, each written for one screen, and most of them loaded everything and returned everything whether you needed it or not. Both the admin panel and the mobile app are built on that API, so nothing about it was easy to change.
What I did
PHP first, and quickly: 8.0 → 8.2 to get enums, and later on to 8.4. Laravel went one major version at a time, each its own deploy — 8 → 9 → 10 → 11 → 12 → 13. The whole climb took about a year, alongside the features the team kept shipping.
Tests came with the work rather than before it. I started on PHPUnit, because Pest wanted a newer Laravel and PHP than I had at that point, and I wrote tests for whatever API I was already touching — a new feature, a bug fix, whatever was open. That’s slower than a coverage sprint, but it’s the version of this that survives a year of feature work, and it took roughly that long to get the bigger part of the API covered.
Once the versions allowed it, the suite moved to Pest. Shorter asserts and a suite you can read are the obvious part; the reason I actually wanted it was database sharding, which splits the tests across parallel jobs in the pipeline and turns the wait into something you don’t plan a coffee around. Pest 5 added test impact analysis on top of that, which is what makes the local loop pleasant.
Then the API itself. I versioned it, left v1 where it was and built v2 as plain CRUD with filtering, sorting and relations loaded on request. Response times were the visible win. The one that matters more is that a new screen no longer needs a new endpoint. The documentation moved to OpenAPI attributes at the same time, because the old one had been out of date for as long as anyone could remember.
Around the code: development came off a shared host that made every build slow and onto Docker, with Portainer on the dev servers. The Bitbucket pipeline now runs PHPStan, Rector, Pint and the Pest suite sharded across jobs. And the branching went from develop, staging and production to trunk-based with feature flags — three long-lived branches is more process than a team this size can pay for.
How it went
The platform ran in production the whole time and no upgrade step needed a rollback. It helps to be honest about why: the user base was still small when the riskiest steps happened. It has grown a long way since, and the same plan today would need more than tests behind it.
The original developers moved on to another project as I took the codebase over, so the whole thing — code, dev infrastructure, pipeline — is mine to answer for. Production is the piece still waiting: it runs on the client’s VPS and deploys by copying files across, with no zero-downtime story and no rollback worth the name. Moving it to Docker and Dokploy is what I’m working towards next.
What I’d do differently
I’d cover an API with tests before changing it, not after. A test that pins down how an endpoint actually behaves is the only thing that stops you breaking the frontend by accident, and I learned that in the order you’d expect. I’d also have written v2 straight away instead of repairing v1 endpoints first — the repairs bought a few months and then got replaced anyway.
The harder part wasn’t technical. The API feeds an admin panel and a mobile app, which means changing how we build APIs meant convincing the two colleagues who consume them. That got much easier after the first v2 endpoint shipped and they could see what it gave them.