End-to-end type safety with Next.js, Prisma, and GraphQL
Tags: nextjs, graphql, webdev, programming
Published: 2021-10-15 20:17:24

**Written by [Alex Ruheni](https://blog.logrocket.com/author/alex_ruheni/)** ✏️

Using consistent types across the entire stack is a major challenge for many development teams. While you might try to define types and interfaces manually, if you have insufficient tooling for detecting changes and throwing errors, changing types in one part of the project could break your entire application.

To provide a better developer experience and reduce overall errors, we can implement end-to-end type safety in our application by using consistent typings across the entire stack.

In this tutorial, we’ll explore end-to-end type safety by building a simple wish list application that allows a user to bookmark items from the internet. We’ll build our type-safe, fullstack application using Next.js, GraphQL, and Prisma.

### Prerequisites

To follow along with this tutorial, you’ll need the following:

*   Node.js installed
*   Basic understanding of [JavaScript and TypeScript](https://blog.logrocket.com/pattern-matching-and-type-safety-in-typescript-1da1231a2e34/)
*   Familiarity with React
*   Familiarity with relational databases
*   Basic understanding of GraphQL

We’ll use the following stack and tools:

*   [Genql](https://genql.now.sh/): a type-safe, GraphQL query builder that provides auto-complete and validation for GraphQL queries
*   Nexus: provides a code-first approach for building GraphQL schemas and type safety in your API layer
*   Prisma: an open source database toolkit that guarantees type safety and simplifies working with relational databases
*   [`apollo-server-micro`](https://www.apollographql.com/docs/apollo-server/integrations/middleware/#apollo-server-micro): HTTP server for the GraphQL API

To follow along with this tutorial, you can view the final code on [GitHub](https://github.com/ruheni/e2e-type-safe-app).

## Project architecture

A common pattern used for building applications is the three-tier architecture, which consists of three components: the presentation layer (frontend), the logic layer (API), and the data layer (database).

To reduce the chances of your application breaking, we’ll need to use consistent data across the three layers and provide validation at each level:

![Three Tier Project Architecture](https://blog.logrocket.com/wp-content/uploads/2021/10/three-tier-project-architecture.png)

To put this in perspective, imagine that while in a database, you make a change to a column within a table.

On applying the schema against the database, the compiler would detect a drift in the types, throwing errors highlighting all of the affected parts of the application in both the frontend and the API. The developer can then apply changes and fix the application’s types.

## Getting started

Navigate to your working directory and initialize your Next.js application by running the following command:

```bash
npx create-next-app --typescript [app-name]
```

Open the new app on the editor of your choice. If you’re using VS Code, you can open the app from the terminal using the `code .` shorthand as follows:

```json
cd [app-name]
code .  #for vs-code users
```

### Install development dependencies

To install development dependencies, run the following commands:

```json
npm install --save-dev prisma @genql/cli ts-node nodemon
```

*   `prisma`: a command line tool used for managing database migrations, generating the database client, and browsing data with Prisma Studio
*   `@genql/cli`: a Genql CLI tool for generating the client that makes GraphQL requests
*   `ts-node`: transpiles TypeScript code into JavaScript
*   `nodemon`: a file watcher for watching our GraphQL API code and regenerating types

```bash
>npm install graphql nexus graphql-scalars @prisma/client apollo-server-micro@2.25.2 @genql/runtime swr
```

*   `nexus`: establishes a code-first approach for building GraphQL APIs
*   `graphql-scalars`: a library that provides custom GraphQL scalar types
*   `@prisma/client`: a type-safe query builder based on your database schema
*   `apollo-server-micro`: a HTTP server for GraphQL APIs
*   `@genql/runtime` and `graphql`: runtime dependencies for Genql
*   `swr` : a lightweight library that provides React Hooks for handling data fetching

Now, create a file called `nexus.tsconfig.json` at the root of your project:

```bash
touch nexus.tsconfig.json
```

To allow regeneration of the schema, add the following code to `nexus.tsconfig.json`:

```json
{
  "compilerOptions": {
    "sourceMap": true,
    "outDir": "dist",
    "strict": true,
    "lib": ["esnext"],
    "esModuleInterop": true
  }
}
```

In your `package.json` file, add the following two scripts, `generate:nexus` and `generate:genql`:

```json
"scripts": {
  //next scripts
  "generate:nexus": "nodemon --exec 'ts-node --transpile-only -P nexus.tsconfig.json pages/api/graphql' --ext 'ts' --watch '*/graphql/**/*.ts'",
  "generate:genql": "nodemon --exec 'genql --schema ./graphql/schema.graphql --output ./graphql/generated/genql'  --watch 'graphql/schema.graphql'" 
}
```

When building the GraphQL API, `generate:nexus` generates types on file changes in the `graphql` folder. When the GraphQL schema is updated, `generate:genql` will regenerate the Genql client.

By default, the types generated by `generate:genql` will be stored in `graphql/generated/genql`, however, you can update the output path as you see fit.

## Setting up the database

To set up Prisma in your project, run the following command:

```bash
npx prisma init
```

The command above creates a new `.env` file and a `prisma` folder at the root of your project. The `prisma` folder contains a `schema.prisma` file for modeling our data.

To use PostgreSQL, the default database provider, update `.env` with a valid connection string pointing to your database. To change providers, simply change the provider in the `datasource db` block in `schema.prisma`. At the time of writing, Prisma supports PostgreSQL, MySQL, SQL Server, and MongoDB as a preview.

When modeling data, Prisma uses the Prisma schema language, which nearly resembles the GraphQL syntax and makes the database schema easy to read and update. For auto-completion and syntax highlighting, you can install the [Prisma VS Code extension](https://marketplace.visualstudio.com/items?itemName=Prisma.prisma).

For the database provider, we’ll use SQLite, however, feel free to use the database provider of choice. Update the provider and URL as follows:

```groovy
datasource db {
  provider = "sqlite"
  url      = "file:./dev.db"
}
generator client {
  provider = "prisma-client-js"
}
```

Let’s create a new model `Item` to map to a table in the database. Add the following fields:

```groovy
/// schema.prisma
model Item {
  id          String  @id @default(cuid())
  title       String
  description String?
  url         String?
  imageUrl    String?
  createdAt   DateTime @default(now())
  updatedAt   DateTime @default(now()) @updatedAt
}
```

Our table has fields for `id`, `title`, `description`, webpage `url`, `imageUrl`, and time stamps for `createdAt` and `updatedAt`.

`id` serves as the primary key for our table, represented by `@id`. The `?` operator denotes that the field is optional, and the default value is `null`. Prisma automatically creates and updates the `createdAt` and `updatedAt` values.

Next, we’ll create a database migration:

```bash
npx prisma migrate dev --name project_init
```

The code above generates a database migration in SQL, which you can find inside of `/prisma/migrations`. After applying the migration against your database, it generates the Prisma Client, which will access the database.

Now, let’s open up Prisma Studio and add some data to test in our application:

```bash
npx prisma studio
```

![New Prisma Studio Project](https://blog.logrocket.com/wp-content/uploads/2021/10/new-prisma-studio-project.png)

Select **Item** **Model** and the **Add record** button to add some data to the database. I chose two items from Amazon:

![Add Date Prisma Studio Database](https://blog.logrocket.com/wp-content/uploads/2021/10/add-date-prisma-studio-database.png)

Click on **Save 2 Changes** to apply the changes.

## Setting up your API

We’ll contain our GraphQL API code in a `graphql` folder at the root of the project, creating a separation of concern. Create the `graphql` folder by running the code below:

```bash
>mkdir graphql
```

In the `graphql` folder, create two files called `schema.ts` and `context.ts`:

```bash
touch graphql/schema.ts graphql/context.ts
```

In your `schema.ts` file, add the following code:

```typescript
// /graphql/schema.ts
import { makeSchema, queryType, mutationType } from "nexus";
import * as path from 'path'

const Query = queryType({
  definition(t) {
    // your queries will go here
  }
})

const Mutation = mutationType({
  definition(t) {
    // your mutations will go here
  }
})

export const schema = makeSchema({
  types: [Query, Mutation],
  outputs: {
    schema: path.join(process.cwd(), 'graphql/schema.graphql'),
    typegen: path.join(process.cwd(), 'graphql/generated/nexus.d.ts'),
  },
  contextType: {
    module: path.join(process.cwd(), 'graphql/context.ts'),
    export: 'Context'
  },
  sourceTypes: {
    modules: [
      {
        module: '@prisma/client',
        alias: 'db'
      }
    ]
  }
})
```

The code snippet above contains the default configuration that we’ll use in the rest of our application. It imports the `makeSchema` method, which defines the following:

*   Output path of your GraphQL schema (default is `graphql/schema.graphql`)
*   Output path of the generated type definitions from Nexus (default is `graphql/generated/nexus.d.ts`)
*   Name and path to the context module
*   `@prisma/client` module that Nexus should use to access the database

The configuration will also empty `queryType` and `mutationType`, where we’ll add our queries and mutations.

In the `graphql/context.ts` file, add the following code:

```typescript
// /graphql/context.ts
import { PrismaClient } from '@prisma/client'

const db = new PrismaClient()

export type Context = {
  db: PrismaClient
}
export const context: Context = {
  db
}
```

The code snippet above:

*   Imports `@prisma/client`
*   Creates a new instance of Prisma Client
*   Creates the context type, which can be replaced with an interface
*   Creates a context object that will add `db` to the GraphQL context, making it available in the GraphQL resolvers

Inside of the `/pages/api/` folder, create a file called `graphql.ts`, which we’ll use to define API routes with Next.js:

```typescript
// /pages/api/graphql.ts
import { ApolloServer } from 'apollo-server-micro'
import { context } from '../../graphql/context'
import { schema } from '../../graphql/schema'

export const config = {
  api: {
    bodyParser: false,
  },
}

const server = new ApolloServer({ schema, context }).createHandler({
  path: '/api/graphql'
})

export default server
```

With the code above, we initialize `apollo-server-micro` and create a handler that will start up the GraphQL Playground whenever a user visits `http://localhost:3000/api/graphql` on the browser.

### Generating GraphQL types

> To avoid running the following three commands simultaneously, you can automate the process using [concurrently](https://www.npmjs.com/package/concurrently). For instructions on installing concurrently, move ahead to the next section.

Now, start the Next.js application server in a terminal window:

```bash
npm run dev
```

In a second terminal window, run the following command to generate your Nexus types and GraphQL schema:

```bash
npm run generate:nexus
```

Finally, in a third terminal window, run the command below to generate the GraphQL types for the frontend:

```bash
npm run generate:genql
```

Inside of `graphql`, a new directory called `generated` is created, which contains the following files:

*   `nexus.d.ts`: contains types automatically generated by Nexus
*   `genql`: generates types located in `graphql/generated/genql`

The contents of this folder will be updated as as you build your GraphQL API.

#### Optional: Setting up concurrently

Let’s install concurrently as a development dependency:

```bash
npm install --save-dev concurrently
```

Add a new script in your `package.json` file that will run both `npm run generate:nexus` and `npm run generate:genql`:

```json
"scripts": {
  //other scripts
  "generate": "concurrently \"npm run generate:nexus\" \"npm run generate:genql\"",
}
```

Now, you can cancel the `npm run generate:nexus` and `npm run generate:genql` scripts and run the new script as follows:

```bash
npm run generate
```

## GraphQL object type

Let’s define a custom `DateTime` GraphQL scalar from the `graph-scalars` library. Nexus provides the `asNexusMethod` property, making the scalar available in the rest of your GraphQL API:

```typescript
// /graphql/schema.ts
import { asNexusMethod, /** other imports */ } from "nexus";
import { DateTimeResolver, } from 'graphql-scalars'

const DateTime = asNexusMethod(DateTimeResolver, 'DateTime')
```

Add the `DateTime` scalar to the GraphQL schema of your API:

```typescript
// /graphql/schema.ts
export const schema = makeSchema({
  types: [/** existing types */, DateTime],
)}
```

Create a new variable `Item` that will define the fields and properties of the GraphQL object type:

```typescript
// /graphql/schema.ts
import { objectType, /** other imports */ } from "nexus";

const Item = objectType({
  name: 'Item',
  definition(t) {
    t.nonNull.id('id')
    t.nonNull.string('title')
    t.string('description')
    t.string('url')
    t.string('imageUrl')
    t.field('createdAt', { type: 'DateTime' })
    t.field('updatedAt', { type: 'DateTime' })
  }
})
```

`objectType` enables you to define GraphQL object types, which are also a `root` type. The `objectType` fields map to the properties and fields in your database.

The `Item` object type has [`id` and `title` as non-nullable fields](https://blog.logrocket.com/defining-types-for-your-graphql-api/). If `nonNull` is unspecified, the fields will be nullable by default. You can update the rest of the fields as you wish.

Update the `types` with the newly created GraphQL object type:

```typescript
// /graphql/schema.ts
export const schema = makeSchema({
  types: [/** existing types */, Item],
)}
```

Every time you update the contents of `types`, the generated types and the GraphQL schema will be updated.

## Enumeration type

Let’s define a `SortOrder` enum value, which we’ll use in the next section to order values in either ascending or descending order:

```typescript
// /graphql/schema.ts
import { enumType, /** other imports */ } from "nexus";

const SortOrder = enumType({
  name: "SortOrder",
  members: ["asc", "desc"]
})

export const schema = makeSchema({
  types: [/** existing types */, SortOrder],
)}
```

## Queries

Queries allow us to read data from an API. Update your `Query` file to contain the following code:

```typescript
// /graphql/schema.ts

const Query = queryType({
  definition(t) {
    t.list.field('getItems', {
      type: 'Item',
      args: {
        sortBy: arg({ type: 'SortOrder' }),
      },
      resolve: async (_, args, ctx) => {
        return ctx.db.item.findMany({
          orderBy: { createdAt: args.sortBy || undefined }
        })
      }
    })

    t.field('getOneItem', {
      type: 'Item',
      args: {
        id: nonNull(stringArg())
      },
      resolve: async (_, args, ctx) => {
        try {
          return ctx.db.item.findUnique({ where: { id: args.id } })
        } catch (error) {
          throw new Error(`${error}`)
        }
      }
    })
  }
})
```

In the code above, we define two queries:

*   `getItems`: returns an `Item` array and allows you to sort the values in either ascending or descending order based on the `createdAt` value
*   `getOneItem`: returns an `Item` based on the ID, a unique value that can’t be null

When writing the database query `ctx.db._query here_`, VS Code provides auto-complete.

## GraphQL mutations

GraphQL mutations are used for manipulating data. Let’s review the three mutations for creating, updating, and deleting data and add them into our application.

### Create

Let’s add a `createItem` mutation in the `Mutation` definition block:

```typescript
t.field('createItem', {
  type: 'Item',
  args: {
    title: nonNull(stringArg()),
    description: stringArg(),
    url: stringArg(),
    imageUrl: stringArg(),
  },
  resolve: (_, args, ctx) => {
    try {
      return ctx.db.item.create({
        data: {
          title: args.title,
          description: args.description || undefined,
          url: args.url || undefined,
          imageUrl: args.imageUrl || undefined,
        }
      })
    } catch (error) {
      throw Error(`${error}`)
    }
  }
})
```

The mutation will accept the following arguments:

*   `title`: compulsory
*   `description`: optional
*   `url`: optional
*   `imageUrl`: optional

If optional values are not provided, Prisma will set the values to `null`. The mutation also returns an `Item` if the GraphQL operation is successful.

### Update

Let’s create an `updateItem` mutation, which accepts similar arguments to the `createItem` mutation, however, <span style="font-family: -apple-system, BlinkMacSystemFont, 'segoe ui', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'helvetica neue', sans-serif;">with a new, compulsory</span> `id` <span style="font-family: -apple-system, BlinkMacSystemFont, 'segoe ui', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'helvetica neue', sans-serif;">argument and an optional</span> `title` <span style="font-family: -apple-system, BlinkMacSystemFont, 'segoe ui', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'helvetica neue', sans-serif;">argument.</span>

If the optional values are not provided, Prisma Client will not update the existing values in the database, using `|| undefined` instead:

```typescript
t.field('updateItem', {
  type: 'Item',
  args: {
    id: nonNull(idArg()),
    title: stringArg(),
    description: stringArg(),
    url: stringArg(),
    imageUrl: stringArg(),
  },
  resolve: (_, args, ctx) => {
    try {
      return ctx.db.item.update({
        where: { id: args.id },
        data: {
          title: args.title || undefined,
          description: args.description || undefined,
          url: args.url || undefined,
          imageUrl: args.imageUrl || undefined,
        }
      })
    } catch (error) {
      throw Error(`${error}`)
    }
  }
})
```

### Delete

Finally, let’s create a `deleteItem` mutation. `deleteItem` expects an `id` argument for the operation to be executed:

```typescript
t.field('deleteItem', {
  type: 'Item',
  args: {
    id: nonNull(idArg())
  },
  resolve: (_, args, ctx) => {
    try {
      return ctx.db.item.delete({
        where: { id: args.id }
      })
    } catch (error) {
      throw Error(`${error}`)
    }
  }
})
```

To test your queries and mutations, you can check out the API on the GraphQL Playground on `http://localhost:3000/api/graphql`:

![Test API Graphql Playground](https://blog.logrocket.com/wp-content/uploads/2021/10/test-api-graphql-playground.png)

As an example, try running the following query on the Playground:

```graphql
query GET_ITEMS {
  getItems {
    id
    title
    description
    imageUrl
  }
}
```

## Interacting with the frontend

Now that we’ve finished setting up our API, let’s try interacting with it from the frontend of our application. First, let's add the following code that reduces the number of times we’ll create a new `genql` instance:

```bash
>mkdir util
touch util/genqlClient.ts
```

Instantiate your client as follows:

```typescript
// /util/genqlClient.ts
import { createClient } from "../graphql/generated/genql"

export const client = createClient({
  url: '/api/graphql'
})
```

The client requires a `url` property, which defines the path of your GraphQL API. Given that ours is a fullstack application, set it to `/api/graphql` and customize it based on the environment.

Other properties include headers and a custom HTTP fetch function that handles requests to your API. For styling, you can add the contents from [GitHub](https://github.com/ruheni/e2e-type-safe-app/blob/main/styles/globals.css) in `global.css`.

## Listing all wishlist items

To list all the items in our wishlist, add the following code snippet to `index.tsx`:

```tsx
// /pages/index.tsx
import Link from 'next/link'
import useSWR from 'swr'
import { client } from '../util/genqlClient'

export default function Home() {
  const fetcher = () =>
    client.query({
      getItems: {
        id: true,
        title: true,
        description: true,
        imageUrl: true,
        createdAt: true,
      }
    })

  const { data, error } = useSWR('getItems', fetcher)

  return (
    <div>
      <div className="right">
        <Link href="/create">
          <a className="btn"> Create Item &#8594;</a>
        </Link>
      </div>
      {error && <p>Oops, something went wrong!</p>}
      <ul>
        {data?.getItems && data.getItems.map((item) => (
          <li key={item.id}>
            <Link href={`/item/${item.id}`}>
              <a>
                {item.imageUrl ?
                  <img src={item.imageUrl} height="640" width="480" /> :
                  <img src="https://user-images.githubusercontent.com/33921841/132140321-01c18680-e304-4069-a0f0-b81a9f6d5cc9.png" height="640" width="480" />
                }
                <h2>{item.title}</h2>
                <p>{item.description ? item?.description : "No description available"}</p>
                <p>Created At: {new Date(item?.createdAt).toDateString()}</p>
              </a>
            </Link>
          </li>
        ))}
      </ul>
    </div>
  )
}
```

`SWR` handles data fetching to the API. `getItems` identifies the query and cached values. Lastly, the `fetcher` function makes the request to the API.

Genql uses a query builder syntax to specify what fields must be returned from a type. `data` is fully typed based on the request that is made.

The query will use an array to pass arguments. The array will contain two objects, the first is passed with the arguments and the second with the field selection:

```typescript
client.query({
    getItems: [
      { sortBy: "asc" },
      {
        id: true,
        title: true,
        description: true,
        url: true,
        imageUrl: true,
        createdAt: true,
      }
    ]
})
```

To query all the fields, you can use the `…everything` object as follows:

```typescript
import { everything } from './generated'

client.query({
  getItems: {
     ...everything
   }
})
```

Alternately, you can use the `chain` syntax to execute requests that specify which arguments and fields should be returned. The `chain` syntax is available on mutations as well:

```typescript
client.chain.query.
  getItems({ sortBy: 'desc' }).get({
    id: true,
    title: true,
    description: true,
    url: true,
    imageUrl: true,
    createdAt: true,
})
```

## Display a single wishlist item

To display a single item in our wishlist, let’s create a new folder in `pages` called `item`. Add a file called `[id].tsx` in the created directory.

The `[_file_name_]` annotation indicates to Next.js that this route is dynamic:

```bash
mkdir pages/item
touch pages/item/[id].tsx
```

Add the following code to your page:

```tsx
// /pages/item/[id].tsx
import { useRouter } from 'next/router'
import useSWR from 'swr'
import Link from 'next/link'
import { client } from '../../util/genqlClient'

export default function Item() {
  const router = useRouter()
  const { id } = router.query

  const fetcher = async (id: string) =>
    client.query({
      getOneItem: [
        { id },
        {
          id: true,
          title: true,
          description: true,
          imageUrl: true,
          url: true,
          createdAt: true,
        }]
    })

  const { data, error } = useSWR([id], fetcher)

  return (
    <div>
      <Link href="/">
        <a className="btn">&#8592; Back</a>
      </Link>
      {error && <p>Oops, something went wrong!</p>}
      {data?.getOneItem && (
        <>
          <h1>{data.getOneItem.title}</h1>
          <p className="description">{data.getOneItem.description}</p>
          {data.getOneItem.imageUrl ?
            <img src={data.getOneItem.imageUrl} height="640" width="480" /> :
            <img src="https://user-images.githubusercontent.com/33921841/132140321-01c18680-e304-4069-a0f0-b81a9f6d5cc9.png" height="640" width="480" />
          }
          {data.getOneItem.url &&
            <p className="description">
              <a href={data.getOneItem.url} target="_blank" rel="noopener noreferrer" className="external-url">
                Check out item &#8599;
              </a>
            </p>
          }
          <div>
            <em>Created At: {new Date(data.getOneItem?.createdAt).toDateString()}</em>
          </div>
        </>
      )
      }
    </div >
  )
}
```

When the page is initialized, the `id` will be retrieved from the route and used as an argument in the `getOneItem` GraphQL request.

## Create an item

To create a new item in our application, let’s create a new page that will serve as the `/create` route by adding the following code:

```bash
touch pages/create.tsx
```

Add the code block below to the file we just created:

```tsx
// /pages/create.tsx
import React, { useState } from "react"
import { useRouter } from "next/router"
import Link from 'next/link'
import { client } from '../util/genqlClient'

export default function Create() {
  const router = useRouter()
  const [title, setTitle] = useState("")
  const [description, setDescription] = useState("")
  const [url, setUrl] = useState("")
  const [imageUrl, setImageUrl] = useState("")
  const [error, setError] = useState()

  const handleSubmit = async (event: React.SyntheticEvent) => {
    event.preventDefault()
    await client.mutation({
      createItem: [{
        title,
        description,
        url,
        imageUrl,
      }, {
        id: true,
      }]
    }).then(response => {
      console.log(response)
      router.push('/')
    }).catch(error => setError(error.message))
  }
  return (
    <>
      {error && <pre>{error}</pre>}
      <Link href="/">
        <a className="btn">&#8592; Back</a>
      </Link>
      <form onSubmit={handleSubmit}>
        <h2>Create Item</h2>
        <div className="formItem">
          <label htmlFor="title">Title</label>
          <input name="title" value={title} onChange={(e) => setTitle(e.target.value)} required />
        </div>
        <div className="formItem">
          <label htmlFor="description">Description</label>
          <input name="description" value={description} onChange={(e) => setDescription(e.target.value)} />
        </div>
        <div className="formItem">
          <label htmlFor="url">URL</label>
          <input name="url" value={url} onChange={(e) => setUrl(e.target.value)} />
        </div>
        <div className="formItem">
          <label htmlFor="imageUrl">Image URL</label>
          <input name="imageUrl" value={imageUrl} onChange={(e) => setImageUrl(e.target.value)} />
        </div>
        <button type="submit"
          disabled={title === ""}
        >Create Item</button>
      </form>
    </>
  )
}
```

In the code snippet above, the form values' state is stored inside of `useState` values. The `handleSubmit` function will be triggered when a user selects the **Create** button. The form values will be retrieved when `handleSubmit` is called.

In the event of an error, the form values will be saved in the error state and displayed to the user.

## Delete an item

Lastly, in the `[id].tsx` page, let’s add an option to delete a wishlist item. When the request is successful, it will return an `id` and navigate to the index route `/`:

```tsx
// /pages/item/[id].tsx

export default function Item() {
  /** exiting code/ functionality */
  const deleteItem = async (id: string) => {
    await client.mutation({
      deleteItem: [{ id }, { id: true }],
    }).then(_res => router.push('/'))
  }

  return (
  <div>
         {data?.getOneItem && (
        <>
          {/* existing code */}
          <button className="delete" onClick={(evt) => deleteItem(data?.getOneItem.id)}>Delete</button>
        </>
      )
      }
    )
}
```

If everything runs correctly, your final application will resemble the image below:

![Final Wishlist Application](https://blog.logrocket.com/wp-content/uploads/2021/10/final-wishlist-application.png)

![Add Item Wishlist Application](https://blog.logrocket.com/wp-content/uploads/2021/10/add-item-wishlist-application.png)

![Delete Wishlist Item](https://blog.logrocket.com/wp-content/uploads/2021/10/delete-wishlist-item.png)

Play around with the application to make sure everything is running as expected. You should be able to view your wishlist items, create a new item, and delete items.

## Conclusion

Congratulations! 🚀 You’ve built an end-to-end, type-safe fullstack application. Following the methodology in this tutorial can reduce errors caused by using inconsistent types. I hope you enjoyed this tutorial!

---

## [LogRocket] (https://logrocket.com/signup): Full visibility into production Next.js apps

Debugging Next applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, [try LogRocket] (https://logrocket.com/signup). 

[![LogRocket Dashboard Free Trial Banner] (https://blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png)](https://logrocket.com/signup)

[LogRocket] (https://logrocket.com/signup) is like a DVR for web apps, recording literally everything that happens on your Next app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your Next.js apps — [start monitoring for free] (https://logrocket.com/signup).