Skip to content

Writing Libraries for the Modern Web

One eternal truth of the web is that it moves lightning-fast, and this past year has been no exception. With ES6 features bringing some of the most dramatic syntax changes we’ve seen in a long time, and the arrival of great developer tools like Babel and Flow, today’s developer process would have been unrecognizable just a few years ago when we released the first edition of our JavaScript SDK.

Earlier today, we open sourced that SDK -- the next step toward open sourcing all our SDKs, as we announced last month. Not only does this new release open the door to contributions from our broader developer community, it also includes a dramatic restructuring of the codebase, designed for the modern web. We used this open source initiative as an opportunity to rewrite the library under-the-hood: decoupling functionality with techniques we’ve previously written about, using ES6 features, and statically typing code. Along the way, we learned a lot about writing and publishing libraries for today’s web ecosystem, building on the experiences of our first ES6 product, Parse+React. Here are some of the tools and methods we used in the process, allowing our engineers to write durable libraries that evolve and adapt to the changing web.

ES6, Inside and Out

ES6 brings a laundry list of features to the JavaScript language, and we use quite a few of them in the source of the JavaScript SDK. We take advantage of module syntax, classes, arrow functions, and more using Babel, a powerful toolkit that compiles future syntax into something legacy browsers can digest. However, Babel isn't just a set of polyfills that will one day be unnecessary — it's an entirely new way of thinking about JavaScript development. TC39 (the standards committee behind JS) has been churning out new language features at an unprecedented rate, and these typically find support in Babel early on. This lets developers try out newly proposed features, and use them long before they're supported in a browser. Web applications will always need to support a long tail of outdated browsers, but Babel helps dissociate the development process from the reality of unsupported features.

When we distribute the Parse SDK, it is already compiled down to ES5 semantics, but we make sure that other developers can take advantage of ES6 features in our code. Previously, the SDK allowed developers to subclass Parse Objects with a Backbone.js-style extends() method. This was great in the days before ES6, but it's completely incompatible with the modern class X extends Y syntax. We rewrote Parse Objects in a way that supports both: they're ES6-compatible classes, but they also support the older subclassing method. This lets legacy code run flawlessly against the library, while new applications can be ready for the future.

Hustle and Flow

  Uncaught TypeError: Cannot read property 'value' of undefined

How many times have you encountered a bug where a variable was unintentionally undefined, or you treated a function as a value? These issues become more likely as systems become more complex, but some of our colleagues at Facebook have been developing a solution. Flow is a static type checker — it parses your code and alerts you to any type mismatches or unsafe code. JavaScript is a highly dynamic language, but Flow lets you annotate your variables with specific types, and ensures that values are exactly what you expect them to be. This means no more writing

var doubleCount = obj.count * 2;

when you meant to write

var doubleCount = obj.count() * 2;

When Flow sees code like that, it'll let you know that it has found a function where it expected a number. In a library with a fairly complex set of layers, like Parse+React, this becomes hugely helpful and prevents developer oversight.

Flow annotations are also great for documenting your code. Glancing at a typed method, it's easy to tell exactly what types of parameters it expects.

function increment(field: string, amount?: number): IncrementOp {

This function takes a string as the first argument, an optional number as the second argument, and returns an instance of an IncrementOp object. Flow annotations go right into your code, and Babel will automatically pull them out when it compiles your library.

If you're releasing a Flow-typed library, you may reach a dilemma: you want to release code that doesn't need to be compiled by your users, but you want them to benefit from your explicit typing. Luckily, between Babel and Flow, there's a solution for that. Babel's flow-comments plugin will leave your types in comment blocks, so that Flow can provide insights on code that can still run in a browser.

Frustration-Free Packaging

You've written thousands of lines of modern, typed JavaScript, and your library is finally ready for release. Before publishing that first npm module and publicizing your Github repo, there are a few things to sort out. For instance, which code goes in each? You have source code, as well as compiled code that will actually run.

Our recommendation: leave the compiled library code out of your version control repository. You wouldn't check in a compiled binary, after all. Before you publish a release to npm, you can build a fresh copy of the compiled library from source. An additional way to maintain a clean library is to use the "files" field of package.json, and only include your final compiled source in the published package.

Before you take the leap and publish that package, you can test exactly how it will look to other developers with the npm pack command. This will zip all packaged files up into a tarball that can then be installed elsewhere with npm. You can use this to ensure that all the necessary files are in the package, and that it can be imported by other applications. Testing before publishing is important, as npm is rather unforgiving — you can remove a broken version, but you can never re-publish code to a specific version number.

Supporting Many Platforms

If you're writing a small library, you should be able to deploy the same code across a wide array of platforms. However, if you find the need to import different dependencies or gate functionality in certain environments (like React Native), things can get a bit more complicated. The Parse SDK tries to provide similar networking and local storage functionality in the browser, Node.js, and React Native. For a time we tried to differentiate these platforms at runtime, but we ran into a number of challenges, especially around packagers. With this latest version of the SDK, we found ourselves special-casing even more code for specific platforms, and we came to a conclusion: just release different builds for different versions.

Using Babel plugins, we inject variables into the code and use dead code elimination to remove disabled branches (take a look at the end of Storage.js for an example). Making a build-time split seemed less than ideal at first, but it allowed us to ensure that our developers got exactly the features they wanted, without having to rely on potentially flaky feature-detection code. With 1.6, the npm module provides three different packages: plain old 'parse' for browsers, 'parse/node' for Node.js, and 'parse/react-native' for React Native Apps. This will let us add more platform-specific features down the road.

These are just some of the tools we used to develop, test, and release the latest Parse JavaScript SDK. It's all open source now, so we encourage you to dive into the source code and build scripts to understand our process. We're looking forward to your pull requests and contributions, as well as the things you'll build with Parse and these developer tools.