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:
- Spend hours debugging a tool's internal configuration and fighting its "opinionated" setup.
- 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 };
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!
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)