DEV Community

ConnelBLAZE
ConnelBLAZE

Posted on

A Simple Crud App with Nextjs, Prisma, SQLite and Flutter

Nextjs – I have been hearing about this React framework for production apps that scale and I wanted to check it out! I decided to go with a contact app and since I’m learning something new, why not learn Tailwind also. So instead of buying one, why not get two for the price of one.
A bit unrelated though but One Piece is The Greatest Anime of All Time!!!
Back to what I was talking about, I’m not here to teach you about Nextjs or Tailwind or Flutter, I’m just showing you what I did and I hope you love it. Let’s understand a few things:
Next.js:
Next.js is a flexible React framework that gives you building blocks to create fast web applications.
Prisma:
Prisma helps app developers build faster and make fewer errors with an open source database toolkit for PostgreSQL, MySQL, SQL Server, SQLite, MongoDB and CockroachDB.
SQLite:
SQLite is a C-language library that implements a small, fast, self-contained, high-reliability, full-featured, SQL database engine. SQLite is the most used database engine in the world. SQLite is built into all mobile phones and most computers and comes bundled inside countless other applications that people use every day.
Flutter:
Flutter transforms the app development process. Build, test, and deploy beautiful mobile, web, desktop, and embedded apps from a single codebase.
Tailwind:
Tailwind - Rapidly build modern websites without ever leaving your HTML.
Now let’s dive right in:

Image description

This is what we’ll be building; a simple contact app that helps you: create a contact, view it, update it and delete it. Let’s create our apps:
Prisma integrates well with Next.js. Next.js combines both frontend code and backend code. This means that we can build the UI components in Next.js and also build our API routes in the same Next.js app, so Next.js contains the client and server. Prisma's being a connection to a database makes it ideal for us to call it in our Next.js app to fetch data from the backend for us. There are many ways in Next.js where we can fetch data. Next.js has three unique functions we can use to fetch data for pre-rendering:
getServerSideProps (Server-side Rendering): Runs when the page is being pre-rendered on each request. We can also call the Prisma client methods here to fetch data that we want to pass to the Next.js components.
npx create-next-app contactsapp (creates the Nextjs App)
Move inside the folder: cd contactsapp. We will install the following dependencies:
npm install -D prisma (add prisma)
npm install @prisma/client (Prisma client for JavaScript. It only runs on Node.js.)
Then run: npx prisma init to initialize Prisma and create a prisma folder inside contactsapp project and a .env file. The prisma folder will contain a schema.prisma file, this is where we declare our Prisma database models. Next, we set up the Prisma connections to our SQLite DB.

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = "file:./dev.db"
}

model Contact {
  id String @id @default(cuid())
  firstname String
  lastname String
  email String
}
Enter fullscreen mode Exit fullscreen mode

This is what I have. Contact is the model we’re working with, you can add more fields if you want to. Next you run migrations to create the Table
npx prisma migrate dev
Now we’ll build our components:
components/AddScreen.js:

import { useForm } from 'react-hook-form';

const FormError = ({ errorMessage }) => {
    return <p className="text-red-300 mt-1">{errorMessage}</p>;
  };


const AddScreen = ({ contacts, AddContactFormProps }) => {
    const { register, handleSubmit, errors } = useForm();
    console.log("The addCContact", AddContactFormProps);
    return (
        <form className="flex flex-col" onSubmit={handleSubmit(AddContactFormProps)}>
            <div className="md:col-span-1 md:flex md:justify-start flex-col bg-slate-900 h-screen text-white px-3">
                <h1 className="font-bold">Add a Contact</h1>
                <div>
                    <div className="mb-4">
                        {/ <InputSpacer> /}
                            <input className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" id="firstname" type="text" placeholder="firstname" name='firstname'
                            {...register('firstname', { required: true })} />
                            {/* {errors.firstName && (
                                <FormError errorMessage="First Name is required" />
                            )} */}
                        {/ </InputSpacer> /}

                    </div>
                    <div className="mb-4">
                        {/ <InputSpacer> /}
                            <input className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" id="lastname" type="text" placeholder="lastname" name='lastname'
                            {...register('lastname', { required: true })} />
                            {/* {errors.lastName && (
                                <FormError errorMessage="First Name is required" />
                            )} */}
                        {/ </InputSpacer> /}
                    </div>
                    <div className="mb-4">
                        {/ <InputSpacer> /}
                            <input className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" id="lastname" type="email" placeholder="email" name='email'
                            {...register('email', { required: true })} />
                            {/* {errors.email && (
                                <FormError errorMessage="First Name is required" />
                            )} */}
                        {/ </InputSpacer> /}
                    </div>
                    <div className="">
                        <button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline w-full" id="btnAdd"
                        type="submit">
                            Add
                        </button>
                    </div>
                </div>
                {/ <Script src="/main.js" strategy='lazyOnload' /> /}
            </div>
        </form>
     );
}

export default AddScreen;
Enter fullscreen mode Exit fullscreen mode

components/DisplayScreen.js:

const DisplayScreen = ({ contacts, delContact }) => {
    console.log(contacts)
    return (
        <div className="px-16 bg-gray-100 md:col-span-2 h-screen">
            <h1 className="font-bold text-slate-800">All Contacts</h1>
            {
                contacts.map(contact => (
                    <div key={contact.id} className="bg-white rounded-md overflow-hidden shadow p-3 my-3 grid grid-cols-3 gap-3">
                        <div className="md:col-span-2">
                            <h1>{ contact.firstname } { contact.lastname }</h1>
                            <span className="text-sm font-bold">{ contact.email }</span>
                        </div>
                        <div className="md:col-span-1">
                            <button className="bg-red-500 text-white p-2 rounded-md hover:scale-125 hover:opacity-80" onClick={
                                () => delContact(contact)
                            }>Del</button>
                        </div>
                    </div>
                ))
            }
        </div>
     );
}

export default DisplayScreen;
Enter fullscreen mode Exit fullscreen mode

pages/index.js:

import { PrismaClient } from '@prisma/client'
import Head from 'next/head'
import { useState } from 'react'
import AddScreen from '../components/AddScreen'
import DisplayScreen from '../components/DisplayScreen'

const prisma = new PrismaClient()

export const getServerSideProps = async () => {
  const contacts = await prisma.contact.findMany()
  return {
    props: {
      initialContacts: contacts
    }
  }
}

const saveContact = async (contact) => {
  const response = await fetch('/api/contacts', {
    method: 'POST',
    body: JSON.stringify(contact),
    headers: {
      'Content-Type': 'application/json; charset=utf8'
    }
  })

  console.log("response", contact)

  if (!response.ok) {
    throw new Error(response.statusText)
  }

  return await response.json()
}

const delContact = async (contact) => {
  if (window.confirm("Do you want to delete this food?")) {
    await fetch('/api/deleteContact', {
      method: 'POST',
      body: JSON.stringify({
        id: contact.id
      }),
      headers: {
        'Content-Type': 'application/json; charset=utf8'
      }
    })
    // console.log("response", contact)
  }
}

export default function Home({ initialContacts }) {
  const [contacts, setContacts] = useState(initialContacts)

  return (
    <div className="">
      <Head>
        <title>Contact App</title>
        <meta name="description" content="Created by Connelblaze" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <div className="grid md:grid-cols-3">

        <AddScreen contacts = { initialContacts } AddContactFormProps = {async (data, e) => {
          try {
            await saveContact(data)
            setContacts([...contacts, data])
            e.target.reset()
          } catch (error) {
            console.log(error);
          }
        }} />

        <DisplayScreen contacts = { initialContacts } delContact = { delContact } />
      </div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

As you can see, we used Fetch to communicate with the APIs
API routes: API routes in Next.js are kept in the pages/api folder. Each file and folder maps to an API endpoint. They are served under the same URL path as the frontend code, localhost:3000. So as localhost:3000/getAllContacts returns all contacts page from the Next.js app.
Now you need to create the API routes

pages/api/getAllcontacts.js:

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

const prisma = new PrismaClient();

export default async (req, res) => {
  const data = req.body;
  try {
    const result = await prisma.contact.findMany();
    res.status(200).json(result);
  } catch (err) {
    console.log(err);
    res.status(403).json({ err: "Error occured." });
  }
};
This returns all contacts.
pages/api/contacts.js:
import { PrismaClient } from "@prisma/client"

const prisma = new PrismaClient();

export default async function handler(req, res) {

    if (req.method !== 'POST') {
        return res.status(405).json({ message: 'Method not allowed' });
      }

      try {

        const conatactData = req.body //JSON.parse(req.body);

        const saveContact = "Contacts "+ conatactData

        res.status(200).json(saveContact)
    } catch (err) {
        console.log("from API error", err)
        res.status(400).json({ message: 'Something went wrong' });
    }
  }
Enter fullscreen mode Exit fullscreen mode

This deletes a contact
Run this: npm run dev and you should have a working example on: localhost:3000.
Now that’s TURF!
Wait a minute I need to recharge, I mean it ran without errors I should be “The God Of Programming”…

Image description

Well that’s a lie! I cried, I regretted my choices, I watched Youtube, I went to Stackoverflow and even had to go look for Nextjs discord channel to get everything working…
Now it is working, let’s get the Flutter side to connect to our Next.js app.
Create a new Flutter app BTW, I used VS Code for Next and Android Studio for Flutter I mean I want to stress my Laptop! Like I said, this isn’t really a tutorial, I’m just showing you what I did for you to be impressed and drop some $... or maybe not, Just to contribute to the Community.
Now I don’t have to go through the various screen, check the Github: Flutter and Nextjs, what I have to say is this: localhost won’t work, so you have to get the IPv4 of your computer and use that instead.
So yeah I know I didn't do the Update part I mean that's why we're doing open source. So why not fork the project up and add that? Huhhh...
Anyway, thanks for Reading and going through, don't forget to like and possibly share!

Buy Me A Coffee

Top comments (0)