DEV Community

Cover image for Build a to-do Node.js CLI with Prisma and Next.js
Omar Moustafa
Omar Moustafa

Posted on • Originally published at devdojo.com

Build a to-do Node.js CLI with Prisma and Next.js

Yo! What's up? Remember the last article where we used Prisma with Next.js and made a CLI and added an authentication feature? I noticed many of you learned a lot! So here?! You will learn even more, including making a to-do list app as a CLI with Prisma and Next.js API powering the application to give it the best functionality!

Hold on, Omar? Why do we need Prisma/Next.js? The reason why buddy is because this application allows you to sync your tasks from anywhere in the world!

Without further time wasting, let's get started.

Creating our API

First of all, we need a database, so let's use PlanetScale for this! Head over to planetscale.com and click Get Started, create your database, and wait for it to get ready for work!

PlanetScale Database Creation

Now, launch your terminal and start a new Next.js project using npx after that change directory:

npx create-next-app
cd to-do-list-app
Enter fullscreen mode Exit fullscreen mode

Now after we created our project, let's add Prisma and get it ready so run the following command in the terminal:

yarn add prisma @prisma/client
yarn prisma init
Enter fullscreen mode Exit fullscreen mode

After that we will get a schema.prisma file in the prisma folder, replace the file with the following:

generator client {
  provider = "prisma-client-js"
  previewFeatures = ["referentialIntegrity"]
}

datasource db {
  provider = "mysql"
  url = env("DATABASE_URL")
  referentialIntegrity = "prisma"
}
Enter fullscreen mode Exit fullscreen mode

So, what we just most importantly changed the database provider to mysql because we are using a MySQL database.

Now on the PlanetScale dashboard, click on Connect and choose Prisma as the option to connect with, copy the .env file provided and create a .env file in your project and paste what you just copied.

Now we will make 2 models, a User and a Task, so add these 2 models to your Prisma schema file:

model User {
  id Int @id @default(autoincrement())
  username String @unique
  password String
  token String @unique
}

model Task {
  id Int @id @default(autoincrement())
  title String
  userToken String
}
Enter fullscreen mode Exit fullscreen mode

Now, your schema file should look like this:

generator client {
  provider = "prisma-client-js"
  previewFeatures = ["referentialIntegrity"]
}

datasource db {
  provider = "mysql"
  url = env("DATABASE_URL")
  referentialIntegrity = "prisma"
}

model User {
  id Int @id @default(autoincrement())
  username String @unique
  password String
  token String @unique
}

model Task {
  id Int @id @default(autoincrement())
  title String
  userToken String
}
Enter fullscreen mode Exit fullscreen mode

Now run yarn prisma db push to sync your MySQL PlanetScale Database.

We have to now create our API files, first remove pages/api/hello.js file and create a file called register.js in pages/api folder and paste the following code:

import { PrismaClient } from '@prisma/client';

export default function handler(req, res) {
    const prisma = new PrismaClient();
    const { username, password, token } = req.body;
    if (!username || !password || !token) {
        res.status(400).json({
            message: 'Missing username, password or token'
        });
        return;
    }
    prisma.user.findUnique({
        where: {
            username: username
        }
    }).then(user => {
        if (user) {
            res.status(400).json({
                message: 'Username already taken'
            });
        } else {
            prisma.user.findUnique({
                where: {
                    token: token
                }
            }).then(user => {
                if (user) {
                    res.status(400).json({
                        message: 'Token already taken'
                    });
                } else {
                    prisma.user.create({
                        data: {
                            username: username,
                            password: password,
                            token: token
                        }
                    }).then(user => {
                        res.json({
                            message: "User created successfully",
                            user: user
                        });
                    });
                }
            }
            );
        }
    });

}
Enter fullscreen mode Exit fullscreen mode

What we are doing here is defining a Prisma client and making variables for username, password, and token. If one of the parameters isn't provided, we return an error:

const prisma = new PrismaClient();
    const { username, password, token } = req.body;
    if (!username || !password || !token) {
        res.status(400).json({
            message: 'Missing username, password or token'
        });
        return;
    }
Enter fullscreen mode Exit fullscreen mode

And for registration we first check if username is used, if yes, we return an error, if not, we check if the token is used, if yes, returns an error as usual, if not, we will proceed to registration:

    prisma.user.findUnique({
        where: {
            username: username
        }
    }).then(user => {
        if (user) {
            res.status(400).json({
                message: 'Username already taken'
            });
        } else {
            prisma.user.findUnique({
                where: {
                    token: token
                }
            }).then(user => {
                if (user) {
                    res.status(400).json({
                        message: 'Token already taken'
                    });
                } else {
                    prisma.user.create({
                        data: {
                            username: username,
                            password: password,
                            token: token
                        }
                    }).then(user => {
                        res.json({
                            message: "User created successfully",
                            user: user
                        });
                    });
                }
            }
            );
        }
    });
Enter fullscreen mode Exit fullscreen mode

Now, we have the user to be able to authenticate/login, create login.js file and paste the following:

import { PrismaClient } from '@prisma/client';

export default function handler(req, res) {
    const prisma = new PrismaClient();
    const { username, password } = req.body;
    if (!username || !password) {
        res.status(400).json({
            message: 'Missing username or password'
        });
        return;
    }
    prisma.user.findUnique({
        where: {
            username: username
        }
    }).then(user => {
        if (user) {
            if (user.password === password) {
                res.json({
                    message: 'User authenticated successfully',
                    userToken: user.token,
                    user: username
                });
            } else {
                res.status(400).json({
                    message: 'Incorrect password'
                });
            }
        } else {
            res.status(400).json({
                message: 'No user found'
            });
        }
    }
    );

}
Enter fullscreen mode Exit fullscreen mode

So as always we are defining the Prisma client and checking if one of the parameters (username or password) are missing, and if so, we will return an error:

    const prisma = new PrismaClient();
    const { username, password } = req.body;
    if (!username || !password) {
        res.status(400).json({
            message: 'Missing username or password'
        });
        return;
    }
Enter fullscreen mode Exit fullscreen mode

Then we use the findUnique feature to use the username which is unique to find a record/user in the table, so if we haven't found a user, we return a error, if we found one with the matching username, we check if the password provided matches along with the username, if the password is correct, we return a token which is something like a random piece of characters & numbers which is hard to guess which allows you to do anything including with the API itself (when sending requests manually), if not, we return an error that the password is incorrect:

prisma.user.findUnique({
        where: {
            username: username
        }
    }).then(user => {
        if (user) {
            if (user.password === password) {
                res.json({
                    message: 'User authenticated successfully',
                    userToken: user.token,
                    user: username
                });
            } else {
                res.status(400).json({
                    message: 'Incorrect password'
                });
            }
        } else {
            res.status(400).json({
                message: 'No user found'
            });
        }
    }
    );
Enter fullscreen mode Exit fullscreen mode

Now, let's make the fun..... start! Create a new file called create.js which will allow the user to create new tasks! Paste the following code into the file:

import { PrismaClient } from '@prisma/client';

export default function handler(req, res) {
    const prisma = new PrismaClient();
    const { token, taskTitle } = req.body;
    if (!token || !taskTitle) {
        res.status(400).json({
            message: 'Missing token or task title'
        });
        return;
    }
    prisma.user.findUnique({
        where: {
            token: token
        }
    }).then(user => {
        if (user) {
            prisma.task.create({
                data: {
                    title: taskTitle,
                    userToken: token
                }
            }).then(task => {
                res.json({
                    message: 'Task created successfully',
                });
            });
        } else {
            res.status(400).json({
                message: 'No user found'
            });
        }
    }
    );
}
Enter fullscreen mode Exit fullscreen mode

Simply skipping defining the Prisma client, so we use findUnique to check the user token if valid or not, if it's not, we pass an error saying that there is no such user, if the user exists, we create a task with the title given and with that token, and then we return a success message:

    prisma.user.findUnique({
        where: {
            token: token
        }
    }).then(user => {
        if (user) {
            prisma.task.create({
                data: {
                    title: taskTitle,
                    userToken: token
                }
            }).then(task => {
                res.json({
                    message: 'Task created successfully',
                });
            });
        } else {
            res.status(400).json({
                message: 'No user found'
            });
        }
    }
    );
Enter fullscreen mode Exit fullscreen mode

Now if we assume someone wants to delete a task? Create delete.js and paste the following:

import { PrismaClient } from '@prisma/client';

export default function handler(req, res) {
    const prisma = new PrismaClient();
    const { token, taskId } = req.body;
    if (!token || !taskId) {
        res.status(400).json({
            message: 'Missing token or task id'
        });
        return;
    }
    prisma.user.findUnique({
        where: {
            token: token
        }
    }).then(user => {
        if (user) {
            prisma.task.findUnique({
                where: {
                    id: taskId
                }
            }).then(task => {
                if (task.userToken === token) {
                    prisma.task.delete({
                        where: {
                            id: taskId
                        }
                    }).then(task => {
                        res.json({
                            message: 'Task deleted successfully',
                        });
                    });
                } else {
                    res.status(400).json({
                        message: 'Task does not belong to the user'
                    });
                }
            }
            );
        } else {
            res.status(400).json({
                message: 'No user found'
            });
        }
    }
    );
}
Enter fullscreen mode Exit fullscreen mode

So what we do is find the user using the token, if we didn't find the user, we return an error, if we found the user, we find the task using the ID, we check if the owner is matching, if so we delete it, if not, we return an error:

    prisma.user.findUnique({
        where: {
            token: token
        }
    }).then(user => {
        if (user) {
            prisma.task.findUnique({
                where: {
                    id: taskId
                }
            }).then(task => {
                if (task.userToken === token) {
                    prisma.task.delete({
                        where: {
                            id: taskId
                        }
                    }).then(task => {
                        res.json({
                            message: 'Task deleted successfully',
                        });
                    });
                } else {
                    res.status(400).json({
                        message: 'Task does not belong to the user'
                    });
                }
            }
            );
        } else {
            res.status(400).json({
                message: 'No user found'
            });
        }
    }
    );
Enter fullscreen mode Exit fullscreen mode

Last but not least the view.js file which allows the user to view the tasks:

import { PrismaClient } from '@prisma/client';

export default function handler(req, res) {
    const prisma = new PrismaClient();
    const { token } = req.body;
    if (!token) {
        res.status(400).json({
            message: 'Missing token'
        });
        return;
    }
    prisma.user.findUnique({
        where: {
            token: token
        }
    }).then(user => {
        if (user) {
            prisma.task.findMany({
                where: {
                    userToken: token
                }
            }).then(tasks => {
                res.json({
                    message: 'Tasks retrieved successfully',
                    tasks: tasks
                });
            });
        } else {
            res.status(400).json({
                message: 'No user found'
            });
        }
    }
    );
}
Enter fullscreen mode Exit fullscreen mode

So what we do here is find the user, if not found, we return an error, if we found the user, we return all the tasks corresponding to the user:

    prisma.user.findUnique({
        where: {
            token: token
        }
    }).then(user => {
        if (user) {
            prisma.task.findMany({
                where: {
                    userToken: token
                }
            }).then(tasks => {
                res.json({
                    message: 'Tasks retrieved successfully',
                    tasks: tasks
                });
            });
        } else {
            res.status(400).json({
                message: 'No user found'
            });
        }
    }
    );
Enter fullscreen mode Exit fullscreen mode

Now, we got our API ready after hard work 🥳!

Creating our CLI

Run the following commands to get started:

mkdir todocli
cd todocli
npm init -y
Enter fullscreen mode Exit fullscreen mode

Now create a index.js file and paste the following:

#!/usr/bin/env node

var args = process.argv.slice(2);
var fs = require('fs');
var request = require('request');
command = args[0];

if (command == 'login') {
    options = {
        url: 'http://localhost:3000/api/login',
        method: 'POST',
        json: {
            username: args[1],
            password: args[2]
        }
    };
    request(options, function (error, response, body) {
        if (!error && response.statusCode == 200) {
            console.log(body.message);
            fs.writeFile('./token.txt', body.userToken, function (err) {
                if (err) {
                    return console.log(err);
                }
                console.log("Token saved successfully!");
            }
            );
        } else {
            console.log(body.message);
        }
    });
} else if (command == 'register') {
    options = {
        url: 'http://localhost:3000/api/register',
        method: 'POST',
        json: {
            username: args[1],
            password: args[2],
            token: Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
        }
    };
    request(options, function (error, response, body) {
        if (!error && response.statusCode == 200) {
            console.log(body.message);
            // save token
            fs.writeFile('./token.txt', body.user.token, function (err) {
                if (err) {
                    return console.log(err);
                }
                console.log("Token saved successfully!");
            }
            );
        } else if (body.message=="Token already taken") {
            console.log("Please try registering again!")
        } else {
            console.log(body.message);
        }
    });
} else if (command == 'create') {
    options = {
        url: 'http://localhost:3000/api/create',
        method: 'POST',
        json: {
            taskTitle: args.slice(1).join(' '),
            token: fs.readFileSync('./token.txt', 'utf8')
        }
    };
    request(options, function (error, response, body) {
        if (!error && response.statusCode == 200) {
            console.log(body.message);
        } else {
            console.log(body.message);
        }
    });
} else if (command == 'view') {
    options = {
        url: 'http://localhost:3000/api/view',
        method: 'POST',
        json: {
            token: fs.readFileSync('./token.txt', 'utf8')
        }
    };
    request(options, function (error, response, body) {
        if (!error && response.statusCode == 200) {
            // save the title and id of each task in an array
            var tasks = [];
            for (var i = 0; i < body.tasks.length; i++) {
                tasks.push({
                    id: body.tasks[i].id,
                    title: body.tasks[i].title
                });
            }
            // print the tasks along with their ids
            console.log("Your tasks are:");
            for (var i = 0; i < tasks.length; i++) {
                console.log(tasks[i].id + ": " + tasks[i].title);
            }

        } else {
            console.log(body.message);
        }
    });
} else if (command == 'delete') {
    options = {
        url: 'http://localhost:3000/api/delete',
        method: 'POST',
        json: {
            taskId: args[1],
            token: fs.readFileSync('./token.txt', 'utf8')
        }
    };
    request(options, function (error, response, body) {
        if (!error && response.statusCode == 200) {
            console.log(body.message);
        } else {
            console.log(body.message);
        }
    });
} else if (command == 'help') {
    console.log("Usage: todocli <command> <arguments>");
    console.log("Commands:");
    console.log("login <username> <password>");
    console.log("register <username> <password>");
    console.log("create <task title>");
    console.log("view");
    console.log("delete <task id>");
    console.log("help");
}
Enter fullscreen mode Exit fullscreen mode

Simply what we are mainly doing is getting the parameters and depending on the API, and the token is saved into a txt file locally.

Now install a required package using npm install request

Now edit your package.json and replace your scripts, paste the following:

  "scripts": {
    "start": "node index.js"
  },
  "bin": {
    "todocli": "index.js"
  }
Enter fullscreen mode Exit fullscreen mode

Your package.json should now look like this:

{
  "name": "todocli",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "bin": {
    "todocli": "index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "request": "^2.88.2"
  }
}
Enter fullscreen mode Exit fullscreen mode

Now we are done, do npm install . -g and start using the app!

Test it out - No setup needed

Just install to-do-cli-prisma with the command:

npm i to-do-cli-prisma
Enter fullscreen mode Exit fullscreen mode

And check how to use it with the command: todocli help

Important links 🔗

Thanks for reading this article and hope you learned something useful!

Top comments (0)