DEV Community

Cover image for Architecture Saved My Project - Swapping an ORM for Pure SQL in Minutes
Renato Silva
Renato Silva

Posted on • Edited on

Architecture Saved My Project - Swapping an ORM for Pure SQL in Minutes

In the world of software development, we are often told that Clean Architecture and SOLID principles are "over-engineering" for small projects. This week, I proved that theory wrong in the most practical way possible: I fired my ORM.

The Goal

I was building a Minimalist Feedback API. The plan was simple: use Prisma 6 with SQLite to persist data. I had already built my Core Domain (Entities) and Business Logic (Use Cases), keeping them isolated from external frameworks.

The Problem: When Tools Fight Back

As I moved to the infrastructure layer, I integrated Prisma 6. However, I ran into unexpected breaking changes and rigid configuration requirements—specifically the deprecation of certain URL properties in the schema files that were previously standard.

I had two choices:

  1. Spend hours debugging a tool's internal configuration and fighting its "opinionated" setup.
  2. Rely on my architecture to swap the tool entirely.

The Pivot: From Prisma to SQLite

Because I used the Repository Pattern, my Use Cases didn't know Prisma existed. They only knew about an abstract interface (or contract) called FeedbackRepository.

I decided to drop the ORM and implement a native SqliteFeedbackRepository using the standard sqlite3 driver.

1. The Repository Swap

I created a new implementation. Note how the logic remains focused only on data persistence, keeping the code clean and predictable:

// src/repositories/sqlite/sqlite-feedback-repository.js
const sqlite3 = require('sqlite3').verbose();
const { FeedbackRepository } = require('../feedback-repository');
const { randomUUID } = require('crypto');

// Connect to the database file
const db = new sqlite3.Database('./database.db');

class SqliteFeedbackRepository extends FeedbackRepository {
  /**
   * Persists a new feedback into the SQLite database
   */
  async create(data) {
    const id = randomUUID();
    const createdAt = new Date().toISOString();

    return new Promise((resolve, reject) => {
      const stmt = db.prepare(
        "INSERT INTO feedbacks (id, name, email, message, createdAt) VALUES (?, ?, ?, ?, ?)"
      );

      stmt.run(id, data.name, data.email, data.message, createdAt, (err) => {
        if (err) return reject(err);
        resolve({ id, ...data, createdAt });
      });

      stmt.finalize();
    });
  }

  /**
   * Retrieves all feedbacks from the database
   */
  async listAll() {
    return new Promise((resolve, reject) => {
      db.all("SELECT * FROM feedbacks ORDER BY createdAt DESC", [], (err, rows) => {
        if (err) return reject(err);
        resolve(rows);
      });
    });
  }
}

module.exports = { SqliteFeedbackRepository };
Enter fullscreen mode Exit fullscreen mode

2. Dependency Injection in Action

​In my server.js, the only change required was the import line. This is the Liskov Substitution Principle in the real world: the ability to replace an implementation with another without breaking the system.

// server.js
// Swapping the implementation is as simple as changing one line:
const { SqliteFeedbackRepository } = require('./repositories/sqlite/sqlite-feedback-repository');

// The rest of the setup remains identical!
const repository = new SqliteFeedbackRepository(); 
const submitFeedback = new SubmitFeedback(repository); // Use Case stays the same!
Enter fullscreen mode Exit fullscreen mode

Why This Matters

​If my database logic had been leaked into my Routes or Use Cases, a change like this would have forced me to rewrite a significant portion of the application.
​Instead, it took me less than 10 minutes to pivot and have a fully working API again.

"Software architecture is the art of postponing decisions until you have more data. Or in this case, the art of making tools replaceable."


Key Takeaways

  • Don't be a hostage to your tools: If a library is causing more friction than value, be ready to replace it.

  • Isolate your Domain: Keep your business rules pure. They should not care if you are using an ORM, a Cloud DB, or a simple text file.

  • Pragmatism > Hype: Sometimes, "boring" technology like native SQL is exactly what you need to keep the project moving forward.

What about you?
Have you ever had to swap a core library mid-project? Let's discuss in the comments!


Check out the live API here: [https://https://minimalist-feedback-api.onrender.com/feedbacks]
(Note: Since this uses a free-tier ephemeral SQLite, the data resets on every deploy)


Final Thoughts

The biggest takeaway from this project was learning how to navigate version incompatibilities between technologies.

I saw firsthand how a solid architecture acts as a safety net. It turned what could have been a complete project restart into a series of manageable, easy-to-execute changes. Tools will always change, but good architecture ensures your business rules remain constant.

Top comments (0)