Why Developers are Moving from Libraries to Frameworks: Future-Proofing Your Apps with Lucia

Discover why developers are shifting from libraries to frameworks for better flexibility and future-proofing. Learn how Lucia's evolution reflects this trend, offering more dynamic, developer-friendly solutions for authentication.

Why Developers are Moving from Libraries to Frameworks: Future-Proofing Your Apps with Lucia

Lucia is an excellent solution for quickly and easily building authentication into a web app. However, as developers, our needs are evolving. We’re increasingly seeking frameworks rather than comprehensive libraries, as they offer more flexibility and adaptability. By opting for a framework, we gain the ability to mix and match tools dynamically, making our applications more adaptable to future changes and ensuring long-term scalability. This approach helps create apps that are not only robust but also future-proof in a fast-evolving development landscape.

Library VS Framework

In the world of software development, understanding the difference between libraries and frameworks is crucial for developers as it can significantly impact how they structure and build their applications. While both libraries and frameworks offer reusable code to help developers solve common problems, they do so in distinct ways.

A library is essentially a collection of functions or classes that developers can call upon when they need them. The developer remains in control of the application's flow, choosing when and how to use the library’s functionality. For instance, the Python requests library allows developers to easily make HTTP requests. When using requests, the developer writes the main logic and decides when and where to fetch data from an API.

On the other hand, a framework provides a structured foundation on which developers build their applications. It dictates much of the architecture and flow, with the developer plugging in their code where necessary. This approach is sometimes referred to as the "inversion of control" because the framework, not the developer, is in charge of the overall flow. For example, Django, a web framework for Python, defines how the components of a web application—like models, views, and templates—should interact. The developer must work within Django's conventions and extend its predefined behavior, meaning the framework dictates the rules, and developers adapt to fit them.

To summarize, libraries provide tools for developers to use, but they are not opinionated about the structure of your application. Frameworks, by contrast, provide a blueprint or skeleton for the application and enforce a certain structure, requiring the developer to follow the framework's patterns. Developers choose libraries when they want more flexibility and control, while they use frameworks when they want a standardized approach with less decision-making about the app's architecture.

Why using Libraries for everything is a bad thing

Using libraries for everything in development can seem like an easy way to speed up a project, but it can quickly lead to issues, especially when it comes to maintainability, performance, and scalability. Here's an example to illustrate why relying too heavily on libraries can be problematic, without referencing UI components:

Example: Building an API with Multiple Libraries

Imagine you're building an API in Node.js. Instead of writing core functionalities yourself, you decide to use libraries for every part of the API: routing, data validation, database access, authentication, and error handling. At first, things work great because each library is doing its job. However, as the project grows, problems start emerging:

1. Dependency Hell

Each library you use may introduce its own set of dependencies, which can lead to conflicts. For example, you choose express for routing, joi for data validation, mongoose for database interaction, and passport for authentication. Over time, you might find that different libraries require different versions of the same package, leading to "dependency hell" where resolving conflicts between packages becomes a nightmare.

If a new feature in your project requires you to upgrade express, you might find that passport is incompatible with the latest version, forcing you to either rewrite parts of your app or stick with an older, unsupported version. This can slow down development and introduce security vulnerabilities.

2. Lack of Consistency

When you use libraries for everything, each one may follow different conventions or paradigms. For instance, the way express handles middleware is very different from how mongoose handles database queries. This lack of consistency can make the codebase harder to follow and maintain, especially for new developers joining the project. When different parts of your app use different patterns, it becomes more challenging to ensure code quality and enforce best practices.

Over time, the team spends more time understanding the idiosyncrasies of each library rather than focusing on writing clear, maintainable code.

3. Performance Bottlenecks

Libraries are often designed to be general-purpose to cater to a wide variety of use cases. This means that they may include a lot of extra functionality that your specific application doesn’t need. For example, if you use lodash to perform basic array manipulations like filtering or finding unique elements, you’re pulling in a massive library when native JavaScript methods (filter(), Set()) could perform the same tasks more efficiently. This can lead to unnecessary performance overhead.

In the API context, using libraries excessively for tasks like validation, authentication, or logging could lead to slower response times, especially when the application grows.

4. Over-reliance and Technical Debt

When you rely on libraries for everything, you're also dependent on their continued maintenance. What if one of the key libraries you're using is no longer maintained? If the maintainers of your chosen authentication library decide to abandon the project or introduce breaking changes, you might be forced to rewrite significant portions of your codebase.

This introduces technical debt—where short-term ease of using a library leads to long-term difficulties in maintaining or upgrading the codebase.

5. Loss of Flexibility

Libraries can limit your flexibility. For instance, when you rely on a library like mongoose for database interaction, you're tightly coupled to MongoDB and the paradigms that mongoose enforces (e.g., schemas and document-based data). If you later decide to switch to a relational database like PostgreSQL, you'll have to refactor a large portion of your codebase because mongoose can't be easily adapted to a SQL-based database.

In this case, if you had written your own database interaction layer, switching databases would be far easier and would involve fewer changes in the application logic.

Conclusion: Balance Libraries with Custom Code

Using libraries isn't inherently bad—many offer tremendous value and save time for specific use cases. However, using libraries for everything can lead to inconsistency, performance issues, dependency conflicts, and technical debt. The key is to find a balance: leverage libraries where they truly add value, but don't shy away from writing custom code when it's simpler, faster, or more maintainable in the long run.

By selectively using libraries and writing your own code where necessary, you can keep your project more maintainable, efficient, and flexible for future changes.

The Future of Lucia: Moving Toward Flexibility and Learning

As the maintainer of Lucia has outlined, the library in its current form will be deprecated early next year. While Lucia initially aimed to provide a simple, low-level authentication solution, the growing complexity surrounding database adapters and the need for more flexibility have led to a rethinking of its role. Rather than continuing as a full-featured authentication library, Lucia will evolve into an open-source educational resource, focused on teaching developers how to implement authentication from scratch in JavaScript.

This shift represents a broader trend: instead of relying on libraries that can become difficult to maintain and extend, developers will be empowered to build their own solutions, making their apps more adaptable and future-proof. The focus will shift toward creating examples for session management, 2FA, password resets, and more—all with the intent of deepening developers' understanding of core authentication concepts.

The current version (v3) will be supported for a few more months to ease the transition, and the migration process should be relatively painless, with no major changes needed to the database schema. However, the maintainers are clear that database adapters may be deprecated sooner due to their inherent complexity.

Looking forward, while there’s potential for Lucia to be transformed into a more framework-like tool similar to Auth.js, the goal will remain to keep things modular, dynamic, and focused on developer education. By moving away from being a dependency-laden library and towards a learning resource, Lucia aims to provide long-term value for developers who want to truly understand the fundamentals of authentication.

As developers, this shift aligns with what we seek: flexibility, control, and the ability to future-proof our applications. By opting for tools and frameworks that give us dynamic choices, we create more scalable and adaptable software—something that Lucia’s evolution is setting the stage for.