Build a Next.js Interactive Web-Based Address Book

Written by

in

In today’s interconnected world, managing contacts efficiently is crucial. Whether you’re a freelancer juggling clients, a small business owner keeping track of leads, or simply someone who likes to stay organized, a digital address book is indispensable. While there are numerous contact management apps available, building your own offers a unique opportunity to tailor the application to your specific needs and learn valuable web development skills. This tutorial will guide you through creating a simple, yet functional, interactive address book using Next.js, a powerful React framework for building modern web applications.

Why Build an Address Book with Next.js?

Next.js provides several advantages that make it an excellent choice for this project:

  • Server-Side Rendering (SSR) and Static Site Generation (SSG): Next.js allows you to pre-render your pages on the server or at build time, improving SEO and initial load times.
  • Routing: Next.js simplifies routing with its file-system-based router, making it easy to create different pages for your application.
  • API Routes: Next.js enables you to create API endpoints within your project, allowing you to handle data fetching and manipulation seamlessly.
  • Developer Experience: Next.js offers a great developer experience with features like hot module replacement and built-in CSS support.

By building an address book with Next.js, you’ll gain practical experience with these key features and learn how to build a performant and user-friendly web application.

Project Overview: What We’ll Build

Our address book will allow users to:

  • Add new contacts with names, phone numbers, and email addresses.
  • View a list of all contacts.
  • Edit existing contact information.
  • Delete contacts.

The application will store contact data in a simple, in-memory data structure (for simplicity in this tutorial). In a real-world scenario, you would likely use a database like MongoDB, PostgreSQL, or a cloud-based service.

Prerequisites

Before we begin, make sure you have the following installed:

  • Node.js and npm (or yarn): You’ll need Node.js and npm (Node Package Manager) or yarn installed on your system. You can download them from the official Node.js website: https://nodejs.org/.
  • A Code Editor: A code editor like Visual Studio Code, Sublime Text, or Atom will be helpful.
  • Basic Understanding of HTML, CSS, and JavaScript: Familiarity with these web technologies is essential.

Step-by-Step Guide

1. Setting Up the Next.js Project

Let’s start by creating a new Next.js project. Open your terminal and run the following command:

npx create-next-app address-book-app

This command will create a new directory called address-book-app with all the necessary files for a Next.js project. Navigate into the project directory:

cd address-book-app

2. Project Structure and File Overview

Your project directory should look something like this:

address-book-app/
├── node_modules/
├── package.json
├── pages/
│   ├── _app.js
│   ├── index.js
│   └── api/
│       └── contacts.js
├── public/
│   └── ...
├── styles/
│   ├── globals.css
│   └── Home.module.css
└── next.config.js
  • pages/: This directory contains all the pages of your application. Each file in this directory represents a route.
  • pages/index.js: This is the home page of your application (accessible at the root path: /).
  • pages/api/: This directory will contain your API routes.
  • public/: This directory is for static assets like images.
  • styles/: This directory contains your CSS files.
  • package.json: This file contains information about your project and its dependencies.

3. Creating the Contact Data Model

Before we start building the UI, let’s define the structure of our contact data. Create a file named contact.js in the root directory of your project and add the following code:

// contact.js

export interface Contact {
  id: string;
  name: string;
  phoneNumber: string;
  email: string;
}

This defines a TypeScript interface (you can also use JavaScript) for a contact object with properties for id, name, phoneNumber, and email. The id will be a unique identifier for each contact. We are using TypeScript for type safety, which is highly recommended for larger projects.

4. Building the Contact List Page (index.js)

Let’s modify the pages/index.js file to display a list of contacts. Replace the existing code with the following:

// pages/index.js
import { useState, useEffect } from 'react';
import styles from '../styles/Home.module.css';
import { Contact } from '../contact';

export default function Home() {
  const [contacts, setContacts] = useState<Contact[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchContacts = async () => {
      try {
        const response = await fetch('/api/contacts');
        if (!response.ok) {
          throw new Error(`HTTP error! Status: ${response.status}`);
        }
        const data = await response.json();
        setContacts(data);
      } catch (error: any) {
        setError(error.message);
      } finally {
        setLoading(false);
      }
    };

    fetchContacts();
  }, []);

  const handleDelete = async (id: string) => {
    try {
      const response = await fetch(`/api/contacts/${id}`, {
        method: 'DELETE',
      });
      if (!response.ok) {
        throw new Error(`HTTP error! Status: ${response.status}`);
      }
      //Refetch contacts after delete
      const updatedContacts = await fetch('/api/contacts').then(res => res.json());
      setContacts(updatedContacts);
    } catch (error: any) {
      setError(error.message);
    }
  };

  if (loading) {
    return <p>Loading contacts...</p>;
  }

  if (error) {
    return <p>Error: {error}</p>;
  }

  return (
    <div className={styles.container}>
      <h1>Address Book</h1>
      <div className={styles.contactList}>
        {contacts.map((contact) => (
          <div key={contact.id} className={styles.contactItem}>
            <p>{contact.name}</p>
            <p>{contact.phoneNumber}</p>
            <p>{contact.email}</p>
            <button onClick={() => handleDelete(contact.id)}>Delete</button>
          </div>
        ))}
      </div>
    </div>
  );
}

Here’s a breakdown of the code:

  • Imports: We import useState and useEffect from React, the CSS styles from Home.module.css, and the Contact interface.
  • State Variables: We use useState to manage the following:
    • contacts: An array to store the contact data.
    • loading: A boolean to indicate whether the data is being loaded.
    • error: A string to store any error messages.
  • useEffect Hook: This hook fetches the contact data from our API route (we’ll create this in the next step) when the component mounts.
  • handleDelete Function: This function is called when the delete button is clicked. It sends a DELETE request to the API route to remove the contact.
  • Conditional Rendering: We use conditional rendering to display a loading message while the data is being fetched and an error message if there’s an issue.
  • Contact List Display: The code iterates over the contacts array and renders each contact’s information.
  • Delete Button: Each contact has a delete button that calls the handleDelete function.

5. Creating the API Route (pages/api/contacts.js)

Now, let’s create the API route that will handle fetching, adding, editing, and deleting contacts. Create a file named contacts.js inside the pages/api/ directory and add the following code:

// pages/api/contacts.js
import { Contact } from '../../contact';
import { v4 as uuidv4 } from 'uuid';

let contacts: Contact[] = [
  {
    id: uuidv4(),
    name: 'John Doe',
    phoneNumber: '123-456-7890',
    email: 'john.doe@example.com',
  },
  {
    id: uuidv4(),
    name: 'Jane Smith',
    phoneNumber: '987-654-3210',
    email: 'jane.smith@example.com',
  },
];

export default function handler(req: any, res: any) {
  if (req.method === 'GET') {
    res.status(200).json(contacts);
  } else if (req.method === 'POST') {
    const newContact: Contact = req.body;
    newContact.id = uuidv4();
    contacts.push(newContact);
    res.status(201).json(newContact);
  } else {
    res.status(405).json({ message: 'Method Not Allowed' });
  }
}

export function deleteContact(id: string) {
  contacts = contacts.filter(contact => contact.id !== id);
}

This code does the following:

  • Imports: We import the Contact interface and the uuidv4 function to generate unique IDs.
  • Contacts Array: We initialize a sample contacts array with some dummy data. This is where your contact data will be stored (in-memory for this tutorial).
  • Handler Function: This is the main function that handles incoming requests to the /api/contacts endpoint.
    • GET Request: If the request method is GET, it returns the contacts array with a 200 OK status.
    • POST Request: If the request method is POST, it creates a new contact using data from the request body, adds it to the contacts array, and returns the new contact with a 201 Created status.
    • Other Methods: For any other HTTP methods (like PUT or DELETE), it returns a 405 Method Not Allowed status.
  • deleteContact Function: This function is called when a contact needs to be deleted.

For deleting a contact, we’ll need a separate API route. Create a file named [id].js inside the pages/api/ directory and add the following code:

// pages/api/[id].js
import { deleteContact } from './contacts';

export default function handler(req: any, res: any) {
  const { id } = req.query;

  if (req.method === 'DELETE') {
    deleteContact(id);
    res.status(200).json({ message: 'Contact deleted' });
  } else {
    res.status(405).json({ message: 'Method Not Allowed' });
  }
}

This code:

  • Imports: Imports the deleteContact function from the contacts.js file.
  • Handles DELETE Request: Checks if the request method is DELETE. If it is, it calls the deleteContact function with the ID from the request parameters and sends a success response.
  • Handles Other Methods: If the request method is not DELETE, it returns a 405 Method Not Allowed status.

6. Adding Styling (styles/Home.module.css)

To make our address book look better, let’s add some basic styling. Open the styles/Home.module.css file and replace the existing content with the following:

/* styles/Home.module.css */
.container {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 20px;
  font-family: sans-serif;
}

.contactList {
  width: 80%;
  margin-top: 20px;
}

.contactItem {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px;
  margin-bottom: 10px;
  border: 1px solid #ccc;
  border-radius: 5px;
}

.contactItem p {
  margin: 0;
}

button {
  padding: 5px 10px;
  background-color: #f00;
  color: white;
  border: none;
  border-radius: 3px;
  cursor: pointer;
}

button:hover {
  background-color: #d00;
}

This CSS provides basic styling for the container, contact list, contact items, and the delete button. Feel free to customize the styles to your liking.

7. Running the Application

Now, start your Next.js development server by running the following command in your terminal:

npm run dev

or

yarn dev

This will start the development server, and you should be able to access your address book at http://localhost:3000. You should see the list of contacts you added in the API route file.

8. Adding the Add Contact Form

Let’s add a form to add new contacts. Modify the pages/index.js file to include the following code within the main <div> element:

<h2>Add Contact</h2>
  <form onSubmit={handleAddContact} className={styles.contactForm}>
    <label htmlFor="name">Name:</label>
    <input type="text" id="name" name="name" value={newContact.name} onChange={handleInputChange} required />

    <label htmlFor="phoneNumber">Phone:</label>
    <input type="tel" id="phoneNumber" name="phoneNumber" value={newContact.phoneNumber} onChange={handleInputChange} />

    <label htmlFor="email">Email:</label>
    <input type="email" id="email" name="email" value={newContact.email} onChange={handleInputChange} />

    <button type="submit">Add Contact</button>
  </form>

Also, add the following code block to the top of pages/index.js, just below the useState hooks:

  const [newContact, setNewContact] = useState<{ name: string; phoneNumber: string; email: string }>({
    name: '',
    phoneNumber: '',
    email: '',
  });

And add the following two functions to the pages/index.js file:


  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setNewContact({ ...newContact, [name]: value });
  };

  const handleAddContact = async (e: React.FormEvent) => {
    e.preventDefault();
    try {
      const response = await fetch('/api/contacts', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(newContact),
      });

      if (!response.ok) {
        throw new Error(`HTTP error! Status: ${response.status}`);
      }

      const addedContact = await response.json();
      setContacts([...contacts, addedContact]);
      setNewContact({ name: '', phoneNumber: '', email: '' }); // Clear the form
    } catch (error: any) {
      setError(error.message);
    }
  };

The code above:

  • Adds a Form: Adds a form with input fields for name, phone number, and email.
  • Handles Input Changes: The handleInputChange function updates the newContact state whenever the user types in any of the input fields.
  • Handles Form Submission: The handleAddContact function is called when the form is submitted. It sends a POST request to the /api/contacts endpoint, adds the new contact to the list and clears the form.
  • Adds State for New Contact: The newContact state holds the values of the input fields.

Add the following CSS to styles/Home.module.css:

.contactForm {
  display: flex;
  flex-direction: column;
  width: 80%;
  margin-top: 20px;
}

.contactForm label {
  margin-bottom: 5px;
}

.contactForm input {
  padding: 8px;
  margin-bottom: 10px;
  border: 1px solid #ccc;
  border-radius: 4px;
}

.contactForm button {
  padding: 10px;
  background-color: #4CAF50;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

Now, when you refresh your app, you should see an “Add Contact” form. You can fill in the form and add new contacts, which will be displayed in the list.

9. Editing Contact Information

To implement the edit functionality, we’ll need a separate page or a modal to display the edit form. For simplicity, we’ll use a modal in this tutorial. First, create a new state variable in pages/index.js for the contact to be edited:

const [editingContact, setEditingContact] = useState<Contact | null>(null);

Then, add a new function to the pages/index.js file:


  const handleEdit = (contact: Contact) => {
    setEditingContact(contact);
  };

Modify the contact item display to include an edit button:


    <div key={contact.id} className={styles.contactItem}>
      <p>{contact.name}</p>
      <p>{contact.phoneNumber}</p>
      <p>{contact.email}</p>
      <button onClick={() => handleDelete(contact.id)}>Delete</button>
      <button onClick={() => handleEdit(contact)}>Edit</button>
    </div>

Add the following code block to pages/index.js, below the “Add Contact” form:


  {editingContact && (
    <div className={styles.modal}>
      <div className={styles.modalContent}>
        <h2>Edit Contact</h2>
        <form onSubmit={handleUpdateContact} className={styles.contactForm}>
          <label htmlFor="name">Name:</label>
          <input type="text" id="name" name="name" value={editingContact.name} onChange={handleEditInputChange} required />

          <label htmlFor="phoneNumber">Phone:</label>
          <input type="tel" id="phoneNumber" name="phoneNumber" value={editingContact.phoneNumber} onChange={handleEditInputChange} />

          <label htmlFor="email">Email:</label>
          <input type="email" id="email" name="email" value={editingContact.email} onChange={handleEditInputChange} />

          <button type="submit">Update Contact</button>
          <button type="button" onClick={() => setEditingContact(null)}>Cancel</button>
        </form>
      </div>
    </div>
  )}

Also, add the following code block to the top of pages/index.js, just below the useState hooks:


  const [editContact, setEditContact] = useState<{ name: string; phoneNumber: string; email: string }>({
    name: '',
    phoneNumber: '',
    email: '',
  });

And add the following two functions to the pages/index.js file:


  const handleEditInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    if (editingContact) {
      setEditingContact({ ...editingContact, [name]: value });
    }
  };

  const handleUpdateContact = async (e: React.FormEvent) => {
    e.preventDefault();
    try {
      const response = await fetch(`/api/contacts/${editingContact?.id}`, {
        method: 'PUT',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(editingContact),
      });

      if (!response.ok) {
        throw new Error(`HTTP error! Status: ${response.status}`);
      }

      const updatedContact = await response.json();
      setContacts(contacts.map(contact => (contact.id === updatedContact.id ? updatedContact : contact)));
      setEditingContact(null); // Close the modal
    } catch (error: any) {
      setError(error.message);
    }
  };

Modify the API route in pages/api/[id].js to handle the PUT request:

// pages/api/[id].js
import { deleteContact } from './contacts';
import { Contact } from '../../contact';

let contacts: Contact[] = [
  {
    id: '1',
    name: 'John Doe',
    phoneNumber: '123-456-7890',
    email: 'john.doe@example.com',
  },
  {
    id: '2',
    name: 'Jane Smith',
    phoneNumber: '987-654-3210',
    email: 'jane.smith@example.com',
  },
];

export default function handler(req: any, res: any) {
  const { id } = req.query;

  if (req.method === 'DELETE') {
    deleteContact(id);
    res.status(200).json({ message: 'Contact deleted' });
  } else if (req.method === 'PUT') {
    const updatedContact: Contact = req.body;
    contacts = contacts.map(contact => (contact.id === id ? { ...contact, ...updatedContact } : contact));
    res.status(200).json(updatedContact);
  } else {
    res.status(405).json({ message: 'Method Not Allowed' });
  }
}

Add the following CSS to styles/Home.module.css:

.modal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 10;
}

.modalContent {
  background-color: white;
  padding: 20px;
  border-radius: 8px;
  width: 80%;
  max-width: 500px;
}

This code adds an edit button to each contact item, which opens a modal with an edit form when clicked. The edit form allows users to modify contact details and save the changes. The updated contact data is then sent to the server to update the contact list.

10. Common Mistakes and How to Fix Them

  • Incorrect API Endpoint URLs: Double-check the URLs used in your fetch calls to ensure they match your API route structure (e.g., /api/contacts).
  • CORS Issues: If you’re fetching data from a different domain, you might encounter Cross-Origin Resource Sharing (CORS) issues. You’ll need to configure CORS on your server to allow requests from your Next.js application. For local development, you might use a browser extension to disable CORS.
  • State Management Errors: Make sure you’re updating state correctly using useState hooks and that you’re not directly modifying state variables. Use the setter functions provided by useState to update the state.
  • Incorrect Data Handling: Ensure that the data you’re sending to and receiving from the API is formatted correctly (e.g., JSON). Use JSON.stringify() when sending data and .json() when parsing the response.
  • Missing Dependencies: If you encounter errors related to missing modules, install them using npm or yarn. For example, if you are not able to import uuid, run npm install uuid.

Key Takeaways

  • Next.js is a powerful framework for building modern web applications with features like server-side rendering, routing, and API routes.
  • Building your own address book allows you to learn practical web development skills and customize the application to your needs.
  • Understanding the basics of React, state management, and API calls is essential for building interactive web applications.
  • TypeScript can improve code maintainability and reduce errors.

FAQ

  1. Can I use a database instead of the in-memory array?

    Yes, absolutely! In a real-world application, you would typically use a database like MongoDB, PostgreSQL, or a cloud-based service (e.g., Firebase, AWS Amplify) to store your contact data. This provides persistence and scalability.

  2. How can I deploy this application?

    You can deploy your Next.js application to various platforms, including Vercel (which is recommended, as it’s the official platform for Next.js), Netlify, AWS, or other hosting providers. The deployment process typically involves pushing your code to a repository and configuring the platform to build and deploy your application.

  3. How can I add search functionality?

    To add search functionality, you can add an input field and use the onChange event to filter the contacts array based on the user’s search input. You’ll need to update the contacts state with the filtered results.

  4. How can I improve the UI/UX?

    You can improve the UI/UX by adding more styling, using UI component libraries (e.g., Material UI, Ant Design, Chakra UI), implementing features like pagination, and providing better error handling and feedback to the user.

Building a web-based address book with Next.js is a great way to learn and practice web development skills. While this tutorial provided a basic foundation, the possibilities for customization and enhancement are vast. From integrating a database to adding advanced features like search, sorting, and user authentication, the journey of building your address book can be a rewarding learning experience. The concepts covered in this tutorial will serve as a solid foundation for tackling more complex web development projects. Remember that the key to mastering web development is consistent practice and exploration. Keep experimenting, and don’t be afraid to try new things. The more you build, the better you’ll become!