Adding a Laravel backend to a Remix App

Adding a Laravel backend to a Remix App

Or How I Learned to Stop Worrying and Love PHP

I’ve been using Remix in production for about 6 months now and have decided to add a Laravel backend to the stack. I wanted to share my experience and what I’ve learned from the process.

Who are you and why should I care?

Hi 👋🏻 my name is Dave Calnan and I write code from sunny Cork, Ireland. By day I work at a startup called Workvivo and by night I run the tech side of a MedTech startup called Teleatherapy. At Teleatherapy, we have a React Native mobile app for patients and a Remix web app for speech therapists.

I’ve been using Remix since it was in a paid supporter preview and it’s my favourite way to ship a React app. Its model just makes sense to me, and it handles lots of the awkward bits of writing a React app better than I ever could: it bridges the network chasm, meaning I can write less code and yet deliver a better user experience. (Less code from me = better UX? Maybe I should just stop programming?)

I chose Remix for Teleatherapy because:

  1. I needed a web app and wanted to write it in React
  2. I needed an API and I discovered I could use Resource Routes to make lightweight API routes
  3. I like using TypeScript and using the same language and types from the backend to the frontend

I chose Prisma as the ORM to manage a Postgres database. I deployed this all to Elastic Beanstalk and RDS on AWS. Why AWS? Because it’s what I know - and also free credits… 😅

What worked well?

  • Loader & action functions - I think some developers start with a database table and work their way forwards, I like to start with a React component and work my way backwards. With Remix I get to write a React component, then add a loader function in the same file, and read directly from the database (in code that only gets executed on the server). It feels like adding a sprinkle of backend and is easier to reason about than having it split across several files. Adding an action function to handle mutations has great ergonomics too.
  • Nested routing - dividing your UI into chunks based on URL segments works really well, and allows Remix to parallelise data fetching in nested components instead of a waterfall of requests you often see in typical apps that fetch data in the client.
  • Community - the Remix community is phenomenal. The Discord is so friendly and helpful. There are some fantastic community packages like remix-auth, remix-validated-form and many more.

What didn’t go well?

For me Remix is the best way to get a React app with data loaded into the hands of my users. However, there are many things it leaves up to you to decide how to implement.

On the one hand, that’s great as it offers so much flexibility and you’ll never have to “fight the framework". But sometimes I guess I wanted to be told what to do a bit more. Stuff like authentication, an ORM, queues and background jobs, etc were up to me.

Remix Stacks are a very cool solution to package together scaffolding, starting points, and services/tools that you’ll need. However as far as I can tell you can’t add a stack to an existing app. As a workaround you can create a new app and copy files and configuration over to your existing app.

None of the things that didn’t work well for me really have to do with Remix.

Remix does what it does so well. It’s the things that it doesn’t prescribe for you (yet?). I knew at some point I would need to send emails, push notifications, process background jobs and more. I figured I would have to plug in a library or service myself to sort them. At the start I was fine with that but each of those would lead to a decision and not just reading the docs to learn the "official" way of doing things. Decisions can be fatiguing and the anxiety of making the right choice is real (for me, anyway).

A real-world example

At one point I needed to archive users. "No problem", I thought. "I’ve done this loads of times before."

  1. Add an archivedAt column to the database
  2. Tell your ORM to filter where: { archivedAt: null } by default and soft delete a record when deleting
  3. Hang on, how do I do that with Prisma?
  4. Oh there is a 2,300 (!) word guide
  5. Based on that guide, choose to either a) Manually add the where clause to every query b) Write custom middleware to intercept queries and modify them

Option b) didn’t feel quite right to me so I went with option a). Of course when adding the where clause to every query, I missed somewhere and ended up showing archived users in the app 🤦🏻‍♂️.

Maybe the grass is always greener, but I just kept thinking in Laravel I would just have added use SoftDeletes to my User model and be done with it. I’ve written enough bugs and been careless enough to know that I could do with being protected from myself here.

Another example

Before launching I realised with a panic that hashed passwords from my User table where by default included in selects and being sent to the client. Of course they were, I never told them not to!

I searched for how to exclude fields with Prisma and found a GitHub issue with 450 reactions asking for this exact feature to be added. I ended up implementing it in user land with a function to exclude properties from the object. Thanks to Remix at least I could do this in a Loader function so the sensitive data never left the server.

Again though, I knew I could do this really easily in Laravel with $hidden = ['hashed_password'] on my User model.

Wanting more batteries

I realised my issues weren’t with Remix but with the other services I had to add myself. I looked into adding other ORMs, or even trying to combine a NodeJS framework with Remix.

In the end I decided to avoid a learning curve and chose Laravel. I’m very familiar and productive with it - it's how I learned to program (thanks Laracasts!) I knew for me it meant I would be able to ship faster and serve as a solid foundation for years to come. People who’ve closely worked with me won’t believe this but it turned out I was willing to compromise on my dream of only writing TypeScript 🥲.

Remix + Laravel? How does that work?

I’m so glad you asked. First of all here’s a high level view of our stack:


Here is what fetching a list of users looked like before, with just Remix:


  • We query the database directly using Prisma from within a loader function that’s executed on the server
  • We have to remember to filter out archived users by adding where: { archivedAt: null }}
  • That data is then passed into the UsersList component without having to handle loading states. (Handling error states is a breeze too with Catch/Error Boundaries)

And here’s what it looks like with Remix + Laravel:


  • Instead of querying the database directly, we call the Laravel API
  • We have to register a route and add a Controller to handle it
  • We add the SoftDeletes trait to the User model and let Laravel's Eloquent ORM filter out archived users unless we ask it not to

But that’s more code?

Shit, you’re right.

It’s now two languages and two servers and so many more files?

Something I’m really coming to appreciate with experience (said the 26-year old 🙄) is that software engineering is all about tradeoffs. Sure, that’s a lot of code to have to write so that I don’t have to remember to write where: { archivedAt: null } but I just know it’s going to save me at some point so it’s worth it for me.

I believe this architecture is the best choice for me, right now, to build this product. I know it inside and out and I know I can ship reliable features quickly for our customers so that’s good enough for me ✌🏻.

What are the downsides of this approach?

  1. Having to program in two languages - there is a bit of context switching and I keep writing stuff like const $variable = 'shit, not again'
  2. Adding an extra layer - I no longer just add a loader function above my React component. I have to register a route, add a controller, and handle the request. That’s a lot more going on but I need an API for the mobile app anyway so thankfully it doesn’t add much more work on top of that.
  3. No more end-to-end type safety? - I thought I would have to manually write type definitions but of course Spatie have my back! I use spatie/laravel-data to make handling API requests and responses much easier and then spatie/typescript-transformer comes along and generates TypeScript types for me to use in my Remix and mobile apps.

How is it working out?

I’m in the process of porting the backend from Remix to Laravel. It’s gone much quicker than I expected. Laravel Sanctum for authentication along with Laravel’s query builder, validation and authorisation have been really helpful.

I’m very aware that I’ve spent about 5 working days so far rewriting stuff that already worked and not shipping features but I really do believe it will save me time and headaches in the long term.

Why didn’t you just do X?

That’s a really good point. I don’t have all the answers so maybe this won't be the best idea but I’m happy with it for now. I’m always happy to learn about alternatives so please do leave a comment.

Does this mean you don’t like Remix, Prisma, etc?

Not at all. I still feel the benefits of using Remix every day and I’m genuinely excited to get my supporter hoodie.

Even since beginning this effort, I made a wedding website for my brother using Remix and Prisma via the Indie Stack. They are fantastic tools and were the obvious choice for me for that project.

None of this has been meant as criticism, just my experience and reasoning for making the particular set of trade-offs I landed on.

You've made me read this for 9 minutes. Anything to say for yourself?

Thanks! You've asked very convenient questions with impeccable timing throughout this whole blogpost.

But for real, I hope this has been at least somewhat coherent and hopefully slightly more insightful.

I'll be writing more about the rewrite; migrating and syncing the old and new database, using Laravel + TypeScript, and maybe even some topics that sound less boring too. If you'd like to read more about those, please feel free to follow the blog.

Thanks for reading,