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!
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
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
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"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
relationMode = "prisma"
}
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
}
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
}
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
});
});
}
}
);
}
});
}
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;
}
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
});
});
}
}
);
}
});
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'
});
}
}
);
}
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;
}
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'
});
}
}
);
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'
});
}
}
);
}
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'
});
}
}
);
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'
});
}
}
);
}
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'
});
}
}
);
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'
});
}
}
);
}
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'
});
}
}
);
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
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");
}
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"
}
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"
}
}
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
And check how to use it with the command: todocli help
Important links 🔗
- GitHub Repo - Omar8345/to-do-list-app-cli
- npm Package - to-do-cli-prisma
Thanks for reading this article and hope you learned something useful!