Why change?

Farewell namespaces, and welcome modules!

Since its inception, the Admin Node.js SDK has offered a stable API structured as a collection of namespaces and service functions. As a result, you might have become familiar with writing code that looks like this:

// Import the global admin namespace
import * as admin from 'firebase-admin';

const app: admin.app.App = admin.initializeApp();
const token: string = await admin.auth().createCustomToken('alice');
const user: admin.auth.UserRecord = await admin.auth().getUser('bob');

Over the past several years the JavaScript and the TypeScript developer ecosystems have made significant advances with regard to writing modular code. The ES6 modules system has become a standard, and is now the preferred means of structuring JS libraries. Modules also enable you to import exactly the set of symbols you wish to use in your code, and avoid long, verbose reference chains.

In order to adopt ES6 best practices and offer a modern Node.js API to our users, we are making some changes to the Admin Node.js SDK. Here’s what the above example would look like with the new Admin SDK APIs:

// Import only what you need
import { initializeApp, App } from 'firebase-admin/app';
import { getAuth, UserRecord } from 'firebase-admin/auth';

const app: App = initializeApp();
const token: string = await getAuth().createCustomToken('alice');
const user: UserRecord = getAuth().getUser('bob');

Notice that you are no longer importing a global admin namespace, which contains all the other functions, types and sub-namespaces. Instead you explicitly import just the things you care about from several module entry points. Also you will no longer need triple-nested type identifiers like admin.auth.UserRecord and admin.database.Reference. Since each type belongs to exactly one module, you can just import them by their short names like UserRecord and Reference. And as typical with ES6 modules, you have the freedom to alias the imports to something more appropriate to your exact coding scenario.

Improvements under the hood

In addition to best practices compliance and a succinct coding style, the proposed API changes also allow us to significantly improve some of the implementation aspects of the Admin SDK. Specifically, this allows us to remove a lot of the complex monkeypatching and dependency lazy loading that’s happening under the hood. Some of these implementation details have been frequent sources of bugs in the past, so overall this cleanup results in a more reliable SDK. It also makes the Admin SDK more amenable to future growth, because we can onboard additional Firebase product APIs as new modules, with no impact on the rest of the SDK.

What about the package size?

A lot of the API changes that are currently being implemented in the Firebase JS SDK for client development are driven by the goal to reduce the final bundle size. This in turn speeds up the page load times of frontend web applications. This is not a major concern for the Admin SDK, since it usually gets deployed in a backend environment that has access to ample resources.

However, the proposed API changes do open the door for us to potentially split the Admin SDK into a set of NPM packages that can be installed independently, thus reducing the SDK installation footprint when it matters. We are not making such structural changes right now (the Admin SDK on-disk installation footprint has been stable around 30MB for a while). But if the need arises in the future, the modularized API provides us a smooth implementation path with no impact on the users.

Why stop at class-level?

In the above example we imported the getAuth() function from the firebase-admin/auth module. This function returns an instance of the Auth class which exposes the Firebase Auth product-level operations like getUser() and createCustomToken(). This is slightly different from the style being implemented in the JS SDK where even the product-level operations are treeshakeable, and therefore can be imported individually. So why the difference?

This again comes down to the matter of bundle size reduction. As a frontend library, the JS SDK is trying to cut down every byte possible from its imports. This does translate into significant gains when the code has to be transferred over a bandwidth constrained network, before it can execute in the end user’s web browser. But this kind of byte-level optimizations do not translate into any meaningful gains in the backend, where the code sits and executes on a server. The initial module load and initialization cost of the Admin SDK is already in the order of 10’s of milliseconds (in typical runtime environments), and doesn’t have a lot of room for improvement. So on this point we have opted to diverge from the JS SDK, and align more with the other server-side Node.js libraries shipped by Google Cloud. Instead of a fully treeshakeable API, the Admin SDK offers a set of types and service functions that can be individually imported. The specific product-level operations are still exposed as methods on a class instance.

This has the added benefit of being a small incremental change, which makes the transition from the old to the new API a lot simpler in terms of syntax:

// Old API
await admin.auth().getUser('bob');

// New API
await getAuth().getUser('bob'); 

What’s happening to the old namespaces?

We plan to support both the old namespace-centric API and the new modular API side-by-side for at least a year. So if you are not ready to migrate yet, you don’t have to do anything, and your existing code will just continue to work. But we do urge developers to migrate to the new modular API as soon as possible, since we want to be able to discontinue namespaces in the Admin SDK at some point.