Part 3 – Full Stack Todos Application with NextJS, Prisma (using SQL Server), and Redux Toolkit (RTK)

The purpose of this document series is to describe the steps that are necessary to create a “Todos” application using NextJS, Prisma, and Redux Toolkit (RTK).

NextJS is an exciting development tool to create web applications because they allow your Server Code and Client Code to be in the same repository. It is essentially like having a NodeJS server application and REACT Application in the same repository.

TLDR, show me the code: https://github.com/woodman231/nextjs-prisma-todos-rest

TOPIC: Configure the REST Client Features

Now we are confident in our REST Server. Let’s now focus on getting our client to connect to the server. Since NextJS is a fullstack framework we will be adding the client files to the same project.

Install Redux Toolkit (RTK)

One of the most popular state management tools for react is the Redux Toolkit (RTK). We will be creating a store for our Todos application and utilizing their createApi and enhanceEndpoints features to connect to the server responses. If you are following along from previously, be sure to do a Ctrl+C to stop the “npm run dev” that you might have done earlier.

Execute the following command to install these tools:

npm install @reduxjs/toolkit react-redux --save
To create the store go in to the “next-app/features/common” folder that we were in earlier. Create a new folder called: “store”, then create a new file called: “index.ts”. Give it the following code:
import { configureStore, ConfigureStoreOptions } from '@reduxjs/toolkit'
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'

export const createStore = (
    options?: ConfigureStoreOptions['preloadedState'] | undefined
) =>
    configureStore({
        reducer: {
        },
    })

export const store = createStore()

export type AppDispatch = typeof store.dispatch
export const useAppDispatch: () => AppDispatch = useDispatch
export type RootState = ReturnType<typeof store.getState>
export const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector
We will be adding to this later, but for now this is at least a blank slate to get an RTK store created from which we will pull the properties of our application state.

Create the base API

First, we will create a base API to use with our application. After the base API is created, we will then add additional endpoints to the api, and make them part of the store.

In the “next-app/features/common/store” folder add a new file called: “api.ts” and give it the following code:

import { createApi, fetchBaseQuery, retry } from '@reduxjs/toolkit/query/react'

// Create our baseQuery instance
const baseQuery = fetchBaseQuery({
  baseUrl: '/api/',
})

const baseQueryWithRetry = retry(baseQuery, { maxRetries: 6 })

/**
 * Create a base API to inject endpoints into elsewhere.
 * Components using this API should import from the injected site,
 * in order to get the appropriate types,
 * and to ensure that the file injecting the endpoints is loaded 
 */
export const api = createApi({
  /**
   * `reducerPath` is optional and will not be required by most users.
   * This is useful if you have multiple API definitions,
   * e.g. where each has a different domain, with no interaction between endpoints.
   * Otherwise, a single API definition should be used in order to support tag invalidation,
   * among other features
   */
  reducerPath: 'applicationApi',
  /**
   * A bare bones base query would just be `baseQuery: fetchBaseQuery({ baseUrl: '/' })`
   */
  baseQuery: baseQueryWithRetry,
  /**
   * Tag types must be defined in the original API definition
   * for any tags that would be provided by injected endpoints
   */
  tagTypes: ['Todos'],
  /**
   * This api has endpoints injected in adjacent files,
   * which is why no endpoints are shown below.
   * If you want all endpoints defined in the same file, they could be included here instead
   */
  endpoints: () => ({}),
})
As you can see here we are telling it to use /api/ as the base URL. Which is one of the neatest features of developing with NextJS is that you don’t need to store different environment configurations for different domains for different API environments since the API routes are all relative to your application. We also defined no endpoints in this file because we will be adding them to other files later.

In the “next-app/features/common/store” folder add a new file called: “provider.tsx” and give it the following code:

"use client";

import React from "react";
import { store } from "./index";
import { Provider } from "react-redux";

export function Providers({ children }: { children: React.ReactNode }) {
  return <Provider store={store}>{children}</Provider>;
}
This is a small bit of code but will become very important glue to make sure that our store is available throughout the application. I also want to say that at this point I am basically turning this application in to a nearly fully client side rendered application when it comes to the UI. In this demonstration no pages will be using server side rendering. The redux toolkit is not available server side as far as I know.

Create the TODOS API

In the “features/todos” folder create a new folder called: “store”. To the “store” folder create a new file called: “todos.ts”. Give it the following code:

import { Prisma } from '@prisma/client'
import { api } from '@/features/common/store/api'
import { TodoDetailsResponseModel } from '@/features/todos/restResponseModels/todoDetailsResponseModel'
import { TodosListResponseModel } from '@/features/todos/restResponseModels/todosListResponseModel'

type TodoUpdateInputWithId = Prisma.TodoUpdateInput & {
    id: number
}

export const todosApi = api.injectEndpoints({
    endpoints: (builder) => ({
        getTodos: builder.query<TodosListResponseModel, void>({
            query: () => 'todos',
        }),
        getTodo: builder.query<TodoDetailsResponseModel, number>({
            query: (id) => `todos/${id}`,
        }),
        createTodo: builder.mutation<TodoDetailsResponseModel, Prisma.TodoCreateInput>({
            query: (body) => ({
                url: 'todos',
                method: 'POST',
                body
            })
        }),
        updateTodo: builder.mutation<TodoDetailsResponseModel, TodoUpdateInputWithId>({
            query: (data) => {
                const { id } = data;
                const dataToPut: Prisma.TodoUpdateInput = {
                    title: data.title,
                    dueDate: data.dueDate,
                    done: data.done
                }
                return {
                    url: `todos/${id}`,
                    method: 'PUT',
                    body: dataToPut
                }
            }
        }),
        deleteTodo: builder.mutation<undefined, Number>({
            query: (id) => {
                return {
                    url: `todos/${id}`,
                    method: 'DELETE'
                }
            }
        })
    }),
})

export const {
    useGetTodosQuery,
    useGetTodoQuery,
    useCreateTodoMutation,
    useUpdateTodoMutation,
    useDeleteTodoMutation,
} = todosApi

export const {
    endpoints: { 
        getTodos,
        getTodo,
        createTodo,
        updateTodo,
        deleteTodo
    }
} = todosApi
As you can see here we are importing the API from the store in the common feature that we created earlier. We are now adding endpoints to that api. We have defined two queries and 3 mutations. The two queries are to get the list of todos and to get the todo details. The mutations are to create, update, and delete a todo. As you can tell by the URLs they match the URLs that we created within the “api” folder of the “app” folder. The types also match the types for the RequestBodies and ResponseBodies that are expected to be provided to the API.

Something interesting here is that I had to add an id parameter to the update request. The reason for that is that the query parameter only takes one type, and that type must have the id in it to specify the URL that we want to PUT. But I was also able to remove that ID before performing the PUT by manually specifying the fields of the request body.

For more information about creating queries and mutation see these links:
https://redux-toolkit.js.org/rtk-query/usage/queries
https://redux-toolkit.js.org/rtk-query/usage/mutations

Update the store to use the API Reducer

Return to “features/common/store/index.ts” Give it the following code:

import { configureStore, ConfigureStoreOptions, getDefaultMiddleware } from '@reduxjs/toolkit'
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import { api } from './api';

export const createStore = (
    options?: ConfigureStoreOptions['preloadedState'] | undefined
) =>
    configureStore({
        reducer: {
            [api.reducerPath]: api.reducer
        },
        middleware: (getDefaultMiddleware) =>
            getDefaultMiddleware().concat(api.middleware),
        ...options,
    })

export const store = createStore()

export type AppDispatch = typeof store.dispatch
export const useAppDispatch: () => AppDispatch = useDispatch
export type RootState = ReturnType<typeof store.getState>
export const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector
With all of this in place we are now ready to use our API with our UI components.

Update the Global Layout to use the Global RTK Store

In the “app” folder there is a “layout.tsx” file. Update it to use this code:

import './globals.css'
import { Inter } from 'next/font/google'
import { Providers } from '@/features/common/store/provider'

const inter = Inter({ subsets: ['latin'] })

export const metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}
Having this provider is what will allow us to do those useStore functions throughout the application. And in our case since we are going to be using the queries and mutations that we defined earlier, this provider is what gives us access to be able to use that code.

Remove boilerplate styles

While we are at it. Let’s remove the customized css stuff from the boilerplate. Go in to global.css and make it such it only has this code:

@tailwind base;
@tailwind components;
@tailwind utilities;
Furthermore, we will be defining our components in a features folder so to the tailwind.config.js file update it to have this code:
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx,mdx}',
    './components/**/*.{js,ts,jsx,tsx,mdx}',
    './app/**/*.{js,ts,jsx,tsx,mdx}',
    './features/**/*.{js,ts,jsx,tsx,mdx}'
  ],
  plugins: [],
}
There was some boilerplate code regarding the theme that was removed from that file as well.

Create the TODOS list page

At this point we are ready to create the TODOS list page. Let’s begin by preparing the components that will be on the list page. The components that will be on the list page include a create button, the list itself, and the items on the list.

Create Button Component

To the “features/todos” folder create a new folder called: “components”. To the components folder create a new folder called: “listOfTodosPageComponents”. To the “listOfTodosPageComponents” folder create a new file called: “createNewTodoButton.tsx”

Give it the following code:

import React from 'react'
import Link from 'next/link'

function CreateNewTodoButton() {
    return (
        <Link href="/todos/create" className='block p-2 m-2 text-white bg-green-500 rounded'>Create New Todo</Link>
    )
}

export default CreateNewTodoButton
For more information about that Link component from next see this page https://nextjs.org/docs/app/building-your-application/routing/linking-and-navigating

TODO List Item Component

To the “features/todos/components/listOfTodosPageComponents” folder create a new file called: “todoListItem.tsx”. Give it the following code:

import React from 'react'
import Link from 'next/link'
import { TodoPayload } from '@/features/todos/prismaPayloads/todoPayload'

interface TodoListItemProps {
    todo: TodoPayload,
    deleteHandler: (id: number) => void
}

function TodoListItem({todo, deleteHandler}: TodoListItemProps) {
    return (
        <div className='flex-1 m-1 p-2 bg-teal-400'>
            <div className='grid p-4 gap-2 grid-rows-2 bg-white rounded'>
                <div>
                    <div className='text-xl font-bold'>{todo.title}</div>
                    <div>Due: {todo.dueDate.toString().split("T")[0]}</div>
                    <div>Done: {todo.done.valueOf().toString()}</div>
                </div>
                <div>
                    <div className='grid grid-cols-2 gap-2 bg-white'>
                        <Link href={`/todos/edit/${todo.id}`} className='block p-2 m-2 text-white bg-blue-500 rounded'>Edit</Link>
                        <button className='p-2 m-2 text-white bg-red-500 rounded' onClick={() => deleteHandler(todo.id)}>Delete</button>
                    </div>
                </div>
            </div>
        </div>
    )
}

export default TodoListItem
Take note that we were able to use the Payload that we used on the server side project since they are the same model. This really provides us an extra convenience of not having to define a new model for the client side project.

TODOS List Component

In the “features/todo/components/listOfTodosPageComponents” folder create a new file called: “listOfTods.tsx”. Give it the following code:

import React from 'react'
import TodoListItem from './todoListItem'
import { TodoPayload } from '@/features/todos/prismaPayloads/todoPayload'

interface ListOfTodosProps {
    todos: TodoPayload[],
    deleteHandler: (id: number) => void
}

function ListOfTodos(props: ListOfTodosProps) {
    return (
        <div className='flex flex-col p-6 bg-gray-400'>
            {props.todos.map((todo) => (
                <TodoListItem key={todo.id} todo={todo} deleteHandler={props.deleteHandler} />
            ))}
        </div>
    )
}

export default ListOfTodos
Take note that once again we were able to use the payload used earlier.

TODOS List Page Component

To the “features/todo” folder create a new folder called: “pages”. In the “pages” folder create a new file called: “listOfTodosPage.tsx” and give it the following code:

import React from 'react'
import { useGetTodosQuery, useDeleteTodoMutation } from '@/features/todos/store/todos'
import CreateNewTodoButton from '@/features/todos/components/listOfTodosPageComponents/createNewTodoButton'
import ListOfTodosComponent from '@/features/todos/components/listOfTodosPageComponents/listOfTodos'

function ListOfTodosPage() {
    const { isFetching, isError, error, isSuccess, data, refetch } = useGetTodosQuery(undefined, {
        refetchOnMountOrArgChange: true
    });

    const [deleteTodo, {isSuccess: deleteSuccess}] = useDeleteTodoMutation();

    const deleteHandler = (id: number) => {
        const confirmed = confirm("Are you sure you want to delete this todo?");
        if (confirmed) {
            deleteTodo(id);
        }        
    }

    React.useEffect(() => {
        if (deleteSuccess) {
            refetch();
        }
    }, [deleteSuccess, refetch])

    return (
        <div>
            <h1 className="text-3xl">List of Todos</h1>
            {isFetching && <div>Loading...</div>}
            {isError && <div>{error.toString()}</div>}
            {isSuccess && data && data.data && (
                <>
                    <CreateNewTodoButton />
                    <ListOfTodosComponent todos={data.data} deleteHandler={deleteHandler} />
                </>
            )}
        </div>
    )
}

export default ListOfTodosPage
This is where using the power of the RTK create api starts to show. Notice that from the todos store we imported the useGetTodosQuery, and the useDeleteTodoMutation. The properties and methods available from the useGetTodosQuery are pretty powerful. In some instances there are people that try and manually handle and set those isFetching, isError, isSuccess, and etc properties. Using the createApi with RTK makes those properties for us. When we first call that useGetTodosQuery it does indeed beging the fetching process at that point. We also have a mutation, the useDeleteTodoMutation which only executes when we ask it to. You will also notice that we included a refetchOnMountOrArgChange property to true in the useGetTodosQuery. This is important otherwise it will just have stale data the entire time we are in the application. Not having that on might be useful for enums or things that don’t change that often. Or you can also specify a polling time to continually refresh instead. For more information on those features see this page https://redux-toolkit.js.org/rtk-query/usage/queries.

Add the TODOS List Page Component to the Route

To the “app” folder create a new folder called; “todos”. To the “todos” folder create a new file called: “page.tsx”. Give it the following code:

"use client";
import ListOfTodosPage from '@/features/todos/pages/listOfTodosPage'

export default ListOfTodosPage
This is what will officially make the /todos/ page render in our app.

Update the “app/page.tsx” file with the following code:

import Link from "next/link"

export default function Home() {
  return (
    <main className="p-2">
      <h1 className="text-3xl">Welcome</h1>
      <p>
        <Link className="underline text-blue-600 hover:text-blue-800 visited:text-purple-600" href="/todos">Manage Todos</Link>
      </p>
    </main>
  )
}

Test the TODOS List Page

At this point you should be able to start up your application (either npm run dev, or a combination of npm run build with npm run start). And then browse to the TODOS List Page. Depending on what kind of testing you ended up with during the POSTMAN testing that was done earlier. You may see a screen similar to this:

None of the buttons work yet, but at least you should see the list. You can even add and delete more through POSTMAN as described earlier and refresh this screen to see the results.

Create the Create TODO Page

Let’s begin by creating some components that will be common for the create and edit forms.

Create the ID Form Control

To the “features/todos/components” folder create a new folder called: “createOrEditFormControls”. Create a new file called: “idFormControl.tsx”. Give it the following code:

import React from 'react'

interface IdFormControlProps {
    defaultValue: number
}

function IdFormControl({ defaultValue }: IdFormControlProps) {
    return (
        <input type="hidden" name="id" value={defaultValue} />
    )
}

export default IdFormControl

Create the Title Form Control

To the “features/todos/components/createOrEditFormControls” folder create a new file called: “titleFormControl.tsx”. Give it the following code:

import React from 'react'

interface TitleFormControlProps {
    defaultValue: string
}

function TitleFormControl({ defaultValue }: TitleFormControlProps) {

    const [title, setTitle] = React.useState(defaultValue);

    return (
        <>
            <label
                className="p-2 m-2 text-white bg-green-500 rounded"
                htmlFor="title">
                Title
            </label>
            <input
                className="p-2 m-2 text-black border border-gray-500 rounded"
                type="text"
                name="title"
                id="title"
                value={title}
                onChange={(e) => setTitle(e.currentTarget.value)}
            />
        </>
    )
}

export default TitleFormControl

Create the Due Date Form Control

To the “features/todos/components/createOrEditFormControls” folder create a new file called: “dueDateFormControl.tsx”. Give it the following code:

import React from 'react'

interface DueDateFormControlProps {
    defaultValue: string
}

function DueDateFormControl({ defaultValue }: DueDateFormControlProps) {

    const [dueDate, setDueDate] = React.useState(defaultValue);

    return (
        <>
            <label
                className="p-2 m-2 text-white bg-green-500 rounded"
                htmlFor="dueDate">
                Due Date
            </label>
            <input
                className="p-2 m-2 text-black border border-gray-500 rounded"
                type="date"
                name="dueDate"
                id="dueDate"
                value={dueDate}
                onChange={(e) => setDueDate(e.currentTarget.value)}
            />
        </>
    )
}

export default DueDateFormControl

Create the Done Form Control

To the “features/todos/components/createOrEditFormControls” folder create a new file called: “doneFormControl.tsx”. Give it the following code:

import React from 'react'

interface DoneFormControlProps {
    defaultValue: boolean
}

function DoneFormControl({ defaultValue }: DoneFormControlProps) {

    const [done, setDone] = React.useState(defaultValue);

    return (
        <>
            <label
                className="p-2 m-2 text-white bg-green-500 rounded"
                htmlFor="done">
                Done
            </label>
            <input
                className="p-2 m-2 text-black border border-gray-500 rounded"
                type="checkbox"
                name="done"
                id="done"
                checked={done}
                onChange={(e) => setDone(e.currentTarget.checked)}
            />
        </>
    )
}

export default DoneFormControl

Create the Submit Button Form Control

To the “features/todos/components/createOrEditFormControls” folder create a new file called: “submitButton.tsx”. Give it the following code:

import React from 'react'

function SubmitButton() {
  return (
    <input type="submit" value="Submit" className="p-2 m-2 text-white bg-green-500 rounded" />
  )
}

export default SubmitButton

Create the Create Form Component

To the “features/todos/components” folder create a new file called; “createTodoForm.tsx”. Give it the following code:

import React from 'react'
import TitleFormControl from './createOrEditFormControls/titleFormControl'
import DueDateFormControl from './createOrEditFormControls/dueDateFormControl'
import DoneFormControl from './createOrEditFormControls/doneFormControl'
import SubmitButton from './createOrEditFormControls/submitButton'

interface CreateTodoFormProps {
    handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void
}

function CreateTodoForm({ handleSubmit }: CreateTodoFormProps) {
    return (
        <form className="flex flex-col" onSubmit={handleSubmit}>
            <TitleFormControl defaultValue="" />
            <DueDateFormControl defaultValue="" />
            <DoneFormControl defaultValue={false} />
            <SubmitButton />
        </form>
    )
}

export default CreateTodoForm

Create the Create Form Page Component

To the “features/todos/pages” folder create a new file called: “createTodoPage.tsx”. Give it the following code:

"use client";
import React from 'react'
import CreateTodoForm from '@/features/todos/components/createTodoForm'
import { useCreateTodoMutation } from '@/features/todos/store/todos'
import { useRouter } from 'next/navigation';

function CreateTodoPage() {

    const router = useRouter();
    const [createTodo, { isError, isSuccess }] = useCreateTodoMutation();

    const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        const formData = new FormData(e.currentTarget);
        const title = formData.get('title') as string;
        const dueDate = formData.get('dueDate') as string;
        const done = formData.get('done') as string;
        createTodo({
            title,
            dueDate,
            done: done === 'on'
        })
    }

    React.useEffect(() => {
        if (isError) {
            alert('Error creating todo');
        }
        if (isSuccess) {
            router.push("/todos");
        }
    }, [isError, isSuccess, router])

    return (
        <div>
            <h1 className="text-3xl">Create Todo</h1>
            <CreateTodoForm handleSubmit={handleSubmit} />
        </div>
    )
}

export default CreateTodoPage
This is where the rubber hits the road again using the useCreateTodoMutation from the store that we created earlier. We intercept the submit and prevent default (which goes to a different page, which in a SPA we do not want). Then we gather the information from the form and send the properties along to the createTodo method. If things work out, we re-route them to the todos list page. If things don’t, we send an alert specifying why.

Add the Create TODO Page to the Route

To the “app/todos” folder create a new folder called: “create”. Create a new file called: “page.tsx”. Give it the following code:

"use client";
import CreateTodoPage from "@/features/todos/pages/createTodoPage";

export default CreateTodoPage;

Create the Update TODO Page

With the components that we made earlier this should be a little simpler.

Create the Edit Form Component

To the “features/todos/components” folder create a new file called: “editTodoForm.tsx”. Give it the following code:

import React from 'react'
import IdFormControl from './createOrEditFormControls/idFormControl'
import TitleFormControl from './createOrEditFormControls/titleFormControl'
import DueDateFormControl from './createOrEditFormControls/dueDateFormControl'
import DoneFormControl from './createOrEditFormControls/doneFormControl'
import SubmitButton from './createOrEditFormControls/submitButton'

interface EditTodoFormProps {
    defaultValues: {
        id: number,
        title: string,
        dueDate: string,
        done: boolean
    },
    handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void
}

function EditTodoForm({ defaultValues, handleSubmit }: EditTodoFormProps) {
    return (
        <form className="flex flex-col" onSubmit={handleSubmit}>
            <IdFormControl defaultValue={defaultValues.id} />
            <TitleFormControl defaultValue={defaultValues.title} />
            <DueDateFormControl defaultValue={defaultValues.dueDate} />
            <DoneFormControl defaultValue={defaultValues.done} />
            <SubmitButton />
        </form>
    )
}

export default EditTodoForm

Create the Edit Form Page Component

To the “features/todos/pages” folder create a new file called: “updateTodoPage.tsx”. Give it the following code:

import React from 'react'
import EditTodoForm from '../components/editTodoForm';
import { useUpdateTodoMutation, useGetTodoQuery } from '@/features/todos/store/todos'
import { useRouter } from 'next/navigation';
import { IDParams } from '@/features/common/params/idParams';
import { idParamaterValidator } from '@/features/common/paramValidators/idParamaterValidator';

function UpdateTodoPage({ params }: IDParams) {

    const validationResult = idParamaterValidator({ params });
    if (!validationResult.isValid) {
        throw new Error("Invalid id parameter");
    }

    const router = useRouter();
    const [updateTodo, { isError, isSuccess }] = useUpdateTodoMutation();
    const { data, isFetching } = useGetTodoQuery(Number(params.id), {
        refetchOnMountOrArgChange: true
    });

    const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        const formData = new FormData(e.currentTarget);
        const id = formData.get('id') as string;
        const title = formData.get('title') as string;
        const dueDate = formData.get('dueDate') as string;
        const done = formData.get('done') as string;
        updateTodo({
            id: Number(id),
            title,
            dueDate,
            done: done === 'on'
        })
    }

    React.useEffect(() => {
        if (isError) {
            alert('Error updating todo');
        }
        if (isSuccess) {
            router.push("/todos");
        }
    }, [isError, isSuccess, router])

    return (
        <div>
            <h1 className="text-3xl">Update Todo</h1>
            {
                isFetching ? <p>Loading...</p> : (
                    data && data.data &&
                    <EditTodoForm
                        defaultValues={{
                            id: Number(params.id),
                            title: data.data.title,
                            dueDate: new Date(data.data.dueDate.toString()).toISOString().split('T')[0],
                            done: data.data.done
                        }}
                        handleSubmit={handleSubmit} />
                )
            }
        </div>
    )
}

export default UpdateTodoPage
What was nice here is that I was able to use the IDParams that we used on the server side. I was also able to use the validateIdParams that we used on the server side, but on this client side component. Throwing that error displays things within the error boundary in Nextjs. See this page for more details on that https://nextjs.org/docs/app/building-your-application/routing/error-handling.

Add the Update TODO Page to the Route

To the “app” folder create a new folder called: “edit”. To the “edit” folder create a new folder called: “[id]”. In that folder create a new file called: “page.tsx”. Give it the following code:

"use client";
import UpdateTodoPage from '@/features/todos/pages/updateTodoPage';

export default UpdateTodoPage

Test the entire Application

At this point you should be able to test out the entire application. All create, read, update, delete and list functionality.

Conclusion

NextJS is certainly a powerful platform to create full stack web applications. While it does provide functionality to create server side rendered pages, it also provides functionality to create static site, and single page applications. This demonstration was mostly in regards to a single page application. All routing and rendering were performed client side, while the fetching of data from the database was all performed via REST Calls. Combining Prisma and RTK Query you are able to use the same models for REST Responses, and do not need to manually rekey them in. If you follow this practice, when the database changes, the types that both the client and server recognize will change as well.

GitHub Repo: https://github.com/woodman231/nextjs-prisma-todos-rest

Parts:

Part 1 – Create a Development Container, Create the Next App and Install Required Dependencies
Part 2 – Configure the REST Server Features
Part 3 – Configure the REST Client Features

About Intertech

Intertech is a Software Development Consulting Firm that provides single and multiple turnkey software development teams, available on your schedule and configured to achieve success as defined by your requirements independently or in co-development with your team. Intertech teams combine proven full-stack, DevOps, Agile-experienced lead consultants with Delivery Management, User Experience, Software Development, and QA experts in Business Process Automation (BPA), Microservices, Client- and Server-Side Web Frameworks of multiple technologies, Custom Portal and Dashboard development, Cloud Integration and Migration (Azure and AWS), and so much more. Each Intertech employee leads with the soft skills necessary to explain complex concepts to stakeholders and team members alike and makes your business more efficient, your data more valuable, and your team better. In addition, Intertech is a trusted partner of more than 4000 satisfied customers and has a 99.70% “would recommend” rating.