Tim Addison

Writing a simple Pocket app in Node.js with no dependencies

As I’ve started using Pocket to track articles I’ve read/would like to read, I wanted to build a workflow that I could use when writing my Link Roundups. At the same time, I’ve been wondering how productive you can be with no dependencies. A simple Pocket app to download my saved articles and write them to a file felt like a good test.

The source code is available on GitHub as PocketDumper - the rest of this article focuses on the thought process rather than how it works.

This is my personal experience for a set of tools that has limited requirements. On larger projects, or in your tools - you may find the dependencies, frameworks, etc. to be invaluable. They have a place and the no dependencies approach is definitely not appropriate everywhere!

Why no dependencies?

Building tools to make my life easier is something I’ve been doing for a long time. As those tools have aged they occasionally require moderate to heroic efforts to get them working again.

A few months ago I moved from Windows to macOS and I’ve had a lot of tools that were built against platform-specific toolchains or dependencies suddenly demand a lot of effort to get them working. If switching OS is rare, I’d rank encountering issue with dependency updates on almost anything built against a modern web toolchain/stack to be common. The blog post I wrote about updating babel-eslint to @babel/eslint-parser remains the most popular post on my site by a large margin - I don’t think I’m alone in suffering from upgrade hell.

The tools that have required the least TLC are shell scripts (luckily for me recent versions of PowerShell run cross-platform), though the developer experience is not ideal - especially when they start to grow into miniature applications rather than tools.

Containers seem to be the logical answer here, but I’ve experienced both compatibility issues with the move to Apple Silicon and the - in my mind - larger issue that the first-run time can be atrocious (time and energy spent to run what amounts to a trivial script).

If I was a stronger C/C++ programmer I think I’d be writing all of my tools in that language. Although I’m not building my tools for the long now, I’d wager that if they were in C++ they’d probably work just fine in a hundred years. Instead, I’ve settled on JavaScript and Node.js.

How can you say no dependencies and then pick Node? Haven’t you heard about node_modules?

It has been a very long time since I’ve started a node project and not immediately installed multiple packages (or started from a template/framework such as create-react-app or Next.js). Even with Next’s efforts to reduce dependencies there’s still 150+ dependencies installed along for a brand new project. On top of that, I can’t think of the last project I created where I didn’t start off with one of either TypeScript or Babel plus a bundler.

But what if we gave all that up, what are we left with?

Well, as of Node 17.5+ - it turns out we can get quite a lot done.

My Setup

I’ve cheated a little on the no dependencies front by leveraging Volta to provide me with access to nodemon in my toolchain. I’m making the assumption that JavaScript and Node are going to be around for a long time, and so global tools like nodemon will be accessible. Even if Volta disappears, I’m confident npm install -g will still be an option.

To get type checking without TypeScript I’m using VS Code with a jsconfig file. This doesn’t give me anywhere near the same experience as I get with a full TypeScript project, but it definitely gives me enough to miss common errors - type annotations are still possible with JSDoc comments.

{
  "compilerOptions": {
    "target": "es2021",
    "module": "es2022",
    "checkJs": true
  }
}

I’m also taking advantage of the fact that fetch is included with Node from 17.5+ (behind a flag) to remove the need for node-fetch or similar packages. It’s due to be included without the flag in Node 18.

node --experimental-fetch index.js
// pocketApi.js
const getRequestToken = async function (consumerKey) {
  const response = await fetch(endpoints.GetRequestToken, {
    method: "POST",
    headers,
    body: JSON.stringify({
      consumer_key: consumerKey,
      redirect_uri,
    }),
  })
  /// elided...
}

My index.js file also takes advantage of top-level await which has been available since Node 14.8 as long as the file or project is set to ESM rather than CJS:

// index.js
import { getUserData } from "./util.js"

let { AccessToken, ConsumerKey, Since = 0 } = await getUserData()

Along with this there are now a lot of promise-based APIs available that mean callback’s are no longer needed for things like readline:

import * as readline from "node:readline/promises"
import { stdin as input, stdout as output } from "node:process"

const rl = readline.createInterface({ input, output })
await rl.question("Press enter once you have authorized the application\r\n")

These are just a small set of the enhancements that have landed in Node (and the broader JavaScript ecosystem) in the last few years. It’s a significant amount of effort to get up to speed but looking back at tools I’ve authored 2, 3, or even 5 years ago - they could all be much smaller and take on less dependencies.

I’m not sure I’d go back and rewrite them (excepting some dependency upgrade torture), but for future tools I’ll certainly check what’s available in native node/modern version of JavaScript, before reaching for a dependency.

What’s next?

The only thing I’ve really felt that was missing was a good test runner, although I’m fairly confident that for the kind of tools I’m building I’ll get pretty close with a little bit of assert. I’ve also not had a look at the ecosystem to see if using a test runner via the Volta toolchain might also work for me.

Update: A little research shows that Node 18 is going to get a built-in test-runner. What wonderful timing!

I was initially sceptical I’d get anything useful done with no dependencies. After completing this exercise I reviewed the tools I’ve found enduringly useful and they all involve a fairly small set of operations - filesystem interactions, http calls, and manipulation of in-memory data structures. With ES2022 and Node 18 (particularly if Temporal lands) the surface area of ‘native Node’ has never looked so compelling.