My first serious project in NodeJS: ToRead CLI

Leonardo Teteo - Nov 2 '18 - - Dev Community

As an avid reader, I always have a big list of articles, principally about development, that I intend to read. Development is a fast world and every day more articles pile up coming from newsletters, Twitter, etc. I always looked for a tool where I could put my readings. Some of the apps I tried to use was Pocket, Flipboard, Feedly and other less specialized such as Trello, Google Keep, etc. None of them really satisfied me, the features I wanted to have such as searching by tags and title, archive articles, etc. were offered by these services, but under subscription. As a developer I understand the costs related to an application, but it was not an application important enough to make me subscribe it. Then, I resorted to the greatest advantage of a developer: if you don't like the applications in the market, build your own!

The project is still in an early stage, the features I planned are not all developed yet, all contributions are welcome on Github! :D

Here I will explain a little bit about the structure of the code. This is my first "serious" project in NodeJS, before that I only wrote some scripts to learn and practice the language. It was also the first time I was able to decently unite NodeJS and TypeScript, a language I am also learning that I appreciate very much. Besides TypeScript, the project has the following main dependencies:

  • Babel
  • Jest
  • Rx-Http-Request
  • JSDOM
  • opn
  • Commander.js
  • RxJS
  • chalk

Some of them are very straight-forward and others I will explain my decision throughout the text. Two projects helped me a lot: Taskbook and TypeScript Babel Starter. The first one was the inspiration for this project and some dependencies and design decisions were made based on it. The second was very helpful to me to understand the structure of the project and how to configure Babel to do the job. Many thanks for both!

The project so far has been divided in 5 .ts files each having a separate role. I am trying to divide the responsibilities as much as possible to facilitate expansion and understandability. The first file is index.ts, the main entrance of the application. Using Commander.js I describe all commands in this file, for example the command to list all articles:

Commander
    .command('list')
    .alias('ls')
    .description('List all articles')
    .action(() => {
        Actions.getArticles();
    });

Some of the commands, of course, are more complex and have arguments, but the structure is basically the same and all lead to a method in the Actions class, which leads us to the next file: actions.ts

The actions.ts has the static class Actions, which, as the name implies, implements all the actions of applications such as get the articles, open an article, save an article, etc. For example, above we have Actions.getArticles(), which we can see in detail below:

static storage:Storage = new Storage();

static getArticles() : void{
        let articles:Article[] = this.storage.getArticles();
        articles.forEach(a => {
            Display.printArticle(a, PresentationMode.LIST);            
        });
    }

Generally a method in the Actions class figures classes from the other three files that compose the application: article.ts, storage.ts and display.ts, all of them have very straightforward names. First, the easiest one, article.ts just contains the interface representing an article:

export interface Article{
    id?:number,
    title:string,
    url:string,
    description?:string,
    tags?:string[],
}

The storage.ts is where the Storage class stays, this class is responsible to write the data in a JSON file, my intention was to do something very lightweight, also inspired on the Taskbook project I mentioned. Below a snippet of the class:

    prepareDB(){
        if(!fs.existsSync("file.json")){
            let file : FileStructure = {articles: [], index: 0}
            fs.writeFileSync("file.json", JSON.stringify(file));
        }
    }

    getArticles(): Article[] {
        this.prepareDB();

        let file:FileStructure = JSON.parse(fs.readFileSync("file.json", "utf8"));
        return file.articles;
    }

prepareDB() is always called to create the JSON file if it doesn't exist. And the the rest of the class has methods to do CRUD, for example the getArticles() method above. The entire Storage class is basically depended upon fs library and the JSON constant. Not a single fancy outside dependency is necessary, really, although I plan to improve it, put cryptography if necessary, among other things.

Finally, the display.ts contains the Display class, responsible for everything related to printing on the screen. It uses chalk to get it colorful. As a simple example here is the method that prints an error message:

static printOpenErrorMessage(){
        let message = 'The article was not found. Verify the ID of the article.';
        console.info(chalk`{red.bold ${message}}`);
    }

As I've said before, separation of concerns was the main goal in the infrastructure and sometimes I think that I separated way too much, but I'm good with the way it is going right now. As for the classes and methods itself, I tried to write the code with as less dependencies as possible and as simple as possible, even more so when I'm still learning. Now is a great time to explain some of the dependencies that are still lacking explanation. RxJS and JSDOM, for example, are used when saving a new article in the code below:

static saveArticle(url: string, description: string, tags?: string) : void{

        RxHR.get(url).subscribe(
            (data:any) => {
                if (data.response.statusCode === 200) {
                    let window = (new JSDOM(data.body)).window;
                    let title = window.document.title;
                    let article:Article = {
                        title: title, 
                        url: url,
                        description: description,
                        tags: tags ? tags.split(',') : []
                    };

                    Actions.storage.saveArticle(article);

                    Display.printSaveArticleMessage(data.response.statusCode);
                    Display.printArticle(article, PresentationMode.ONE);
                } else {
                    Display.printSaveArticleMessage(data.response.statusCode);
                }
            },
            (err:any) => console.error(err) // Show error in console
        );
    }

As depicted above, I use RxJS, RxHR and JDOM to make a request to the URL given by the user, get the title of the page and store the article with these information. For me it was the only time it was necessary to RxJS in the entire application, but other opportunities may arise.

Finally, on the testing end I'm using Jest, which I discovered while developing the application and I found if very straightforward in the way the tests and conducted. Maybe it is more functional than what I'm used to in Java, but it still reminds me the way JUnit is used, so it was a smooth sailing using it. An example of test is below:

test('write', () => {    
    let storage = new Storage();
    storage.saveArticle({title: "Teste", url: "http://www.example.com", description: "Description test"})
    expect(fs.existsSync("file.json")).toBe(true);
    fs.unlinkSync("file.json");
});

It has been a great experience to develop this project and I'm looking forward to the opinions of everybody on how I can improve it. Since it was developed as practice in mind I really didn't think about publishing it on NPM, but who knows what the future holds... What do you guys think? Let me know everything!

. . . . . . . . . . . . . . . . . . . . .
Terabox Video Player