Build a Simple Next.js Interactive Survey App: A Beginner’s Guide

In today’s digital landscape, gathering user feedback is crucial for understanding your audience, improving your product, and making data-driven decisions. Surveys provide a direct line of communication with your users, offering valuable insights that can shape your strategy. But building a survey app from scratch can seem daunting, especially if you’re new to web development. This is where Next.js comes in. Next.js, a React framework for production, simplifies the process of building web applications, including interactive surveys, by providing features like server-side rendering, static site generation, and a streamlined development experience.

Why Build a Survey App with Next.js?

Next.js offers several advantages for building a survey application:

  • Performance: Next.js optimizes your application for speed, leading to a better user experience. Server-side rendering and static site generation can significantly improve initial load times.
  • SEO: Server-side rendering makes your content easily crawlable by search engines, improving your website’s visibility.
  • Developer Experience: Next.js provides a great developer experience with features like hot reloading, built-in routing, and easy integration of APIs.
  • Scalability: Next.js is designed to handle applications of all sizes, making it a good choice for both small and large survey projects.
  • Modern Features: Next.js supports modern web development practices like TypeScript, and features like API routes which make the development process smoother.

This tutorial will guide you through building a simple, yet functional, survey application using Next.js. We’ll cover the fundamentals, from setting up your project to implementing features like question types, answer options, and submission handling. By the end, you’ll have a solid understanding of how to build interactive web applications with Next.js.

Setting Up Your Next.js Project

Let’s start by setting up your Next.js project. Open your terminal and run the following command:

npx create-next-app survey-app

This command creates a new Next.js project named “survey-app”. Navigate into your project directory:

cd survey-app

Now, start the development server:

npm run dev

Open your browser and go to http://localhost:3000. You should see the default Next.js welcome page. This confirms that your project is set up correctly.

Project Structure and Basic Components

Next.js uses a file-system-based router. This means that each file in the `pages` directory becomes a route in your application. For example, `pages/index.js` corresponds to the `/` route (the homepage).

Let’s create the basic components for our survey app. We’ll start with the following:

  • `pages/index.js`: The main page of our survey app, which will render the survey questions.
  • `components/SurveyForm.js`: A component that holds the survey form logic and structure.
  • `components/Question.js`: A component to display individual survey questions.

Create these files in your project directory. Here’s how `pages/index.js` might look:

// pages/index.js
import SurveyForm from '../components/SurveyForm';

export default function Home() {
  return (
    <div>
      <SurveyForm />
    </div>
  );
}

This is a simple page that imports the `SurveyForm` component and renders it. Now let’s build the `SurveyForm` component. Here’s a basic implementation for `components/SurveyForm.js`:


// components/SurveyForm.js
import React, { useState } from 'react';
import Question from './Question';

const surveyQuestions = [
  {
    id: 1,
    questionText: 'How satisfied are you with our product?',
    questionType: 'radio',
    options: ['Very Satisfied', 'Satisfied', 'Neutral', 'Dissatisfied', 'Very Dissatisfied'],
  },
  {
    id: 2,
    questionText: 'What features do you use the most?',
    questionType: 'checkbox',
    options: ['Feature A', 'Feature B', 'Feature C'],
  },
  {
    id: 3,
    questionText: 'What could we improve?',
    questionType: 'textarea',
  },
];

function SurveyForm() {
  const [answers, setAnswers] = useState({});

  const handleAnswerChange = (questionId, value) => {
    setAnswers(prevAnswers => ({
      ...prevAnswers,
      [questionId]: value,
    }));
  };

  const handleSubmit = (event) => {
    event.preventDefault();
    console.log('Survey Answers:', answers);
    // Here you would typically send the answers to a server
    alert('Thank you for your feedback!');
  };

  return (
    <form onSubmit={handleSubmit}>
      <h2>Survey</h2>
      {surveyQuestions.map(question => (
        <Question
          key={question.id}
          question={question}
          onAnswerChange={handleAnswerChange}
          answer={answers[question.id]}
        />
      ))}
      <button type="submit">Submit</button>
    </form>
  );
}

export default SurveyForm;

This component manages the survey questions and user responses. It uses the `useState` hook to store the answers and the `handleAnswerChange` function to update the answers when a user selects an option. The `handleSubmit` function logs the answers to the console and can be modified to send the data to a server. Finally, the component renders the questions by mapping over an array of `surveyQuestions` and rendering the `Question` component for each.

Now, let’s create the `Question` component. Here’s an example implementation for `components/Question.js`:


// components/Question.js
import React from 'react';

function Question({ question, onAnswerChange, answer }) {
  const handleChange = (event) => {
    onAnswerChange(question.id, event.target.value);
  };

  switch (question.questionType) {
    case 'radio':
      return (
        <div>
          <p>{question.questionText}</p>
          {question.options.map(option => (
            <div key={option}>
              <label>
                <input
                  type="radio"
                  name={`question-${question.id}`}
                  value={option}
                  checked={answer === option}
                  onChange={handleChange}
                />
                {option}
              </label>
            </div>
          ))}
        </div>
      );
    case 'checkbox':
      return (
        <div>
          <p>{question.questionText}</p>
          {question.options.map(option => (
            <div key={option}>
              <label>
                <input
                  type="checkbox"
                  name={`question-${question.id}-${option}`}
                  value={option}
                  checked={answer?.includes(option)}
                  onChange={(event) => {
                    const value = event.target.value;
                    const newAnswers = answer ? [...answer] : [];
                    if (event.target.checked) {
                      newAnswers.push(value);
                    }
                    else {
                      const index = newAnswers.indexOf(value);
                      if (index > -1) {
                        newAnswers.splice(index, 1);
                      }
                    }
                    onAnswerChange(question.id, newAnswers);
                  }}
                />
                {option}
              </label>
            </div>
          ))}
        </div>
      );
    case 'textarea':
      return (
        <div>
          <p>{question.questionText}</p>
          <textarea
            name={`question-${question.id}`}
            value={answer || ''}
            onChange={handleChange}
          />
        </div>
      );
    default:
      return <p>Unsupported question type</p>;
  }
}

export default Question;

This component renders a single survey question based on its type. It uses a `switch` statement to handle different question types (radio, checkbox, textarea). The `handleChange` function calls the `onAnswerChange` prop (passed from `SurveyForm`) to update the answers in the parent component. Note the implementation of the checkbox question type, which requires more complex logic to handle multiple selections.

Adding Styles with CSS Modules

To keep your styles organized and avoid conflicts, Next.js supports CSS Modules. Create a CSS file with the same name as your component, but with the `.module.css` extension. For example, create `components/SurveyForm.module.css` and `components/Question.module.css`.

Here’s an example of how to use CSS Modules in `components/SurveyForm.module.css`:


/* components/SurveyForm.module.css */
.form {
  display: flex;
  flex-direction: column;
  width: 80%;
  margin: 0 auto;
  padding: 20px;
  border: 1px solid #ccc;
  border-radius: 5px;
}

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

Import the CSS module in your component:


// components/SurveyForm.js
import styles from './SurveyForm.module.css';

And use the styles:


// components/SurveyForm.js
<form className={styles.form} onSubmit={handleSubmit}>
  <button type="submit" className={styles.button}>Submit</button>
</form>

Repeat the same process for the `Question` component, creating a `Question.module.css` file and importing and using the styles within the `Question` component.

Handling Different Question Types

Our `Question` component currently supports three question types: radio, checkbox, and textarea. Let’s explore each one in more detail and consider best practices for handling them.

Radio Buttons

Radio buttons allow users to select only one option from a list. In our `Question` component, we use the `input type=”radio”` element. The `name` attribute is crucial; it links the radio buttons within a question. All radio buttons with the same `name` belong to the same group. The `value` attribute holds the value of the selected option. The `checked` attribute determines which radio button is selected by default and updates dynamically based on the `answer` state.

Checkboxes

Checkboxes allow users to select multiple options. The implementation for checkboxes is slightly more complex than for radio buttons. We need to handle an array of selected values. The code checks if the current option is already in the `answer` array (if `answer` exists), and adds or removes it based on whether the checkbox is checked or unchecked. The `name` attribute should be unique for each checkbox, often including both the question ID and the option value.

Textarea

Textareas allow users to enter free-form text. The `textarea` element is straightforward to implement. We bind the `value` attribute to the `answer` state and use the `onChange` event to update the state as the user types.

Adding More Question Types

To add more question types (e.g., text input, dropdown), you’ll need to extend the `switch` statement in your `Question` component and add the corresponding HTML elements and logic for handling user input. Consider the following when adding new types:

  • Data Structure: Update the `surveyQuestions` array to include the necessary properties for the new question type (e.g., a `placeholder` for text inputs, or a list of `options` for dropdowns).
  • Input Handling: Implement the appropriate event handlers (`onChange`, `onClick`, etc.) to capture user input.
  • State Management: Ensure your `handleAnswerChange` function correctly updates the `answers` state with the user’s input.
  • Validation: Consider adding validation to your input fields to ensure users provide the expected data.

Implementing API Routes for Data Submission

Instead of just logging the survey answers to the console, you’ll likely want to submit them to a server. Next.js provides a convenient way to create API routes. API routes are serverless functions that allow you to handle backend logic directly within your Next.js application.

Create a file named `pages/api/submit-survey.js`:


// pages/api/submit-survey.js
export default async function handler(req, res) {
  if (req.method === 'POST') {
    const { answers } = req.body;

    // Process the answers (e.g., save to a database)
    try {
      // Simulate saving to a database
      console.log('Received survey answers:', answers);
      // In a real application, you would use a database library (e.g., Prisma, Mongoose)
      // to save the data to your database.
      // For example:
      // await prisma.surveyResponse.create({ data: { answers: JSON.stringify(answers) } })
      res.status(200).json({ message: 'Survey submitted successfully!' });
    } catch (error) {
      console.error('Error saving survey data:', error);
      res.status(500).json({ message: 'Error submitting survey' });
    }
  } else {
    res.status(405).json({ message: 'Method Not Allowed' });
  }
}

This code defines an API route that handles POST requests. It receives the survey answers from the request body, logs them to the console, and sends a success or error response. In a real-world application, you would replace the `console.log` with code to save the data to a database. The `try…catch` block handles potential errors during data processing.

To submit the survey data to this API route, modify your `handleSubmit` function in `SurveyForm.js`:


// components/SurveyForm.js
const handleSubmit = async (event) => {
  event.preventDefault();
  try {
    const response = await fetch('/api/submit-survey', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ answers }),
    });

    if (response.ok) {
      alert('Thank you for your feedback!');
      // Optionally, reset the form after successful submission:
      setAnswers({});
    } else {
      alert('There was an error submitting the survey.');
      console.error('Submission error:', await response.text());
    }
  } catch (error) {
    alert('An unexpected error occurred.');
    console.error('Network error:', error);
  }
};

This updated `handleSubmit` function uses the `fetch` API to send a POST request to the `/api/submit-survey` endpoint. It includes error handling to provide feedback to the user in case of submission failures.

Deploying Your Survey App

Once you’ve built your survey app, you’ll want to deploy it so others can access it. Next.js makes deployment straightforward. Popular deployment options include:

  • Vercel: Vercel is the company behind Next.js and offers a seamless deployment experience. It’s optimized for Next.js applications and provides features like automatic deployments, previews, and CDN integration. To deploy to Vercel, you can simply push your code to a Git repository (e.g., GitHub, GitLab, Bitbucket) and import the project into Vercel. Vercel will automatically build and deploy your application.
  • Netlify: Netlify is another popular platform for deploying web applications. It also offers features like automatic deployments, continuous integration, and CDN integration. The deployment process is similar to Vercel: connect your Git repository and Netlify will handle the rest.
  • Other Platforms: You can also deploy your Next.js app to other platforms like AWS, Google Cloud, or Azure. These platforms may require more manual configuration, but offer greater flexibility and control.

For this tutorial, Vercel is highly recommended due to its ease of use and tight integration with Next.js.

Common Mistakes and Troubleshooting

Here are some common mistakes and how to fix them when building a Next.js survey app:

  • Incorrect File Paths: Double-check your file paths, especially when importing components and modules. Typos can easily lead to import errors.
  • CSS Module Issues: If your styles aren’t applying, ensure you’ve correctly imported the CSS module and that you’re using the correct class names. Also, verify that your CSS file is in the right location and the file name matches your component.
  • State Management Errors: Incorrectly updating state can lead to unexpected behavior. Use the correct syntax for updating state with `useState`, especially when dealing with arrays or objects. Make sure you’re using the correct dependencies for your state. For example, if you are expecting the state to be an array, but it is an object, your code will fail.
  • API Route Errors: When submitting data to an API route, make sure your request method is correct (e.g., POST for submitting data). Check your server-side code for errors, such as incorrect database queries or data processing issues. Use `console.log` statements to debug your server-side code.
  • CORS Issues: If you’re encountering CORS (Cross-Origin Resource Sharing) errors when submitting data to an external API, you’ll need to configure your server to allow requests from your domain. This often involves setting the `Access-Control-Allow-Origin` header in your server’s response. For Next.js API routes, this is usually handled automatically, but if you are using a separate backend server, make sure CORS is correctly configured.
  • Deployment Issues: If your app isn’t deploying correctly, check your deployment platform’s documentation for troubleshooting tips. Common issues include incorrect environment variables, build errors, and missing dependencies. Make sure your project is configured correctly for the chosen deployment platform. Verify that your `.gitignore` file includes all the necessary files.
  • Missing Dependencies: Make sure you have installed all the necessary dependencies by running `npm install` or `yarn install` in your project directory.

Best Practices and Optimization

Here are some best practices and optimization tips for your Next.js survey app:

  • Code Organization: Structure your code logically, using components for reusable UI elements and separate files for different functionalities (e.g., API routes, data fetching). Good code organization makes your project easier to maintain and scale.
  • Error Handling: Implement robust error handling throughout your application, including client-side and server-side errors. Provide informative error messages to the user. Use `try…catch` blocks to handle potential errors.
  • Data Validation: Validate user input to ensure data integrity. Use libraries like Yup or Formik for more advanced validation. Consider both client-side and server-side validation.
  • Accessibility: Make your survey app accessible to all users by following accessibility guidelines (e.g., using semantic HTML, providing alt text for images, and ensuring keyboard navigation). Use ARIA attributes where needed.
  • Performance Optimization: Optimize your application for performance by using techniques like code splitting, image optimization, and lazy loading. Use the Next.js built-in image component and consider using a CDN for static assets. Minimize the size of your JavaScript bundles.
  • SEO Optimization: Optimize your app for search engines by using descriptive titles and meta descriptions, generating sitemaps, and using semantic HTML. Take advantage of Next.js’s built-in SEO features.
  • Security: Implement security best practices to protect your application from vulnerabilities. Sanitize user input, protect against cross-site scripting (XSS) attacks, and secure your API routes. Use HTTPS for secure communication.
  • Testing: Write unit tests and integration tests to ensure your code works correctly and to catch bugs early. Use a testing framework like Jest or React Testing Library.
  • Use TypeScript: Consider using TypeScript for your project to improve code maintainability and catch errors early.

Summary / Key Takeaways

Building a survey app with Next.js is a practical and rewarding project for both beginners and experienced web developers. By leveraging Next.js’s features, you can create a performant, SEO-friendly, and maintainable application. This guide has provided you with the foundational knowledge and step-by-step instructions to get started, covering project setup, component creation, styling with CSS Modules, handling different question types, implementing API routes, and deploying your app. Remember to focus on code organization, error handling, data validation, and accessibility to create a polished and user-friendly survey experience. By following these guidelines and continuously learning, you can build powerful and engaging web applications with Next.js.

The journey of building a web application, especially one that allows you to gather valuable insights from your users, is a continuous learning process. Each project, from a simple survey app to more complex web applications, provides new opportunities to refine your skills, explore new technologies, and improve your understanding of web development. Embrace the challenges, learn from your mistakes, and never stop experimenting. The skills you gain from projects like this will serve you well in your future endeavors. The possibilities are endless, and with each line of code, you’re building not just an application, but also your own expertise and a deeper connection with the ever-evolving world of web development.