Static Form Submission To Laravel Using Next.js Api [Part 2]

Reece May • March 11, 2021 • 8 min read

nextjs tutorial react

This post will go through setting up your Next.js application to send your static form data to your Laravel application we setup in part one. If you haven't gone through that one yet, you might want to check it out here.

Overview

Points that I will cover during this article are the following:

  1. Setting up your basic Next.js application
  2. Creating your API handler class
  3. Creating a Simple HTML form to send data to the API
  4. Trying the system out

1. Setting up the Next.js application

Setting up a Next.js application is simple, I would say compared to trying to create a new application, running npx you can do the following: npx create-next-app static-form-front.

This will give you the default things you need to start, you can navigate to that folder and run npm run dev to boot up the dev server. It should be pretty quick.

That brief thing would be about it, if you have any questions, the Vercel docs would describe it better than myself :)

Creating an API function in Next.js

When you create a default application with Next.js you get the pages directory, under there is an index.js file and the api directory, there is a hello.js file, that would result in the URL, https://localhost:3000/api/hello when called.

That's a good thing to remember, it applies to directories names too, so you can create the URL using the directories and file name.

2. API endpoint for handling form submission

We will create our API route by creating a file under the pages/api directory, you can call the file contactus.js (or whatever is better for yourself)

In that file you can place the default code:

export default async function contactus(req, res) {
    //
}

We are going to make sure that this only accepts post requests and fails when the honeypot value happens to be set, it would look like this now:

// api/contactus.js
export default async function contactus(req, res) {

  if (req.method !== 'POST') {
    return res.status(400);
  }

    // honeypot value
  if (req.body?.website) {
    return res.status(200);
  }
}

Then, naturally once we have filtered out those types of requests, we would want to send our data to the Laravel backend code.

We are going to use the included node-fetch with Next.js, which means we can call fetch from within the API.

Because the req body is just a string when posting, you are going to want to parse the body back to JSON to be able to send it to the Laravel application. So far we are going to have the following code for the fetch request:

  let body = JSON.parse(req.body)

  const request = await fetch(
    `${process.env.APP_URL}/api/static/contact`,
    {
      method: 'POST',
      body: JSON.stringify(body),
    }
  )

Now, the application URl can be set via the process.env.APP_URL, this means that it can be changed in the environment variables. You can then set it to the local dev URL for local work or the actual production one without having to hard code it.

But nothing will actually work at this point, you are probably wondering where the API token we generated comes into the mix?? Well, these values are set in the headers value.

We are going to add a headers property to the config value of fetch. The most important part is that we add the same header name we defined in our middleware, that was 'X-Static-Site-Token'. The value will be set via and environment variable to ensure we can change the value without having to hardcode a token value into the application.

    {
        headers: {
            'X-Static-Site-Token': process.env.APP_TOKEN, /** This header is the important point here */
            'Content-Type': 'application/json', /** This header is needed for the POST data to be transformed into the correct structure, not just a string*/
            'Accept': 'application/json',
            "X-Requested-With": "XMLHttpRequest", /** This here, other that the accept, also lets Laravel to send back a JSON response */
        },
    }

Once we have all the parts put together in the API handler, we should have a file that looks like the below version:

// api/contactus.js
export default async function contactus(req, res) {

    if (req?.method !== 'POST') {
        return res.status(400);
    }

        // honeypot value
    if (req?.body?.website) {
        return res.status(200);
    }

    let body = JSON.parse(req.body)

    const request = await fetch(
        `${process.env.APP_URL}/api/static/contact`,
        {
            method: 'POST',
            body: JSON.stringify(body),
            headers: {
                'X-Static-Site-Token': process.env.APP_TOKEN, /** This header is the important point here */
                'Content-Type': 'application/json',
                'Accept': 'application/json',
                "X-Requested-With": "XMLHttpRequest",
            },
        }
    )

    if (request.status !== 201 ) {
        let json = await request.json()

        throw new Error(json?.message || 'Failed to create new message');
    }

    const json = await request.json()

    if (json.errors) {
        console.error(json?.errors)
        throw new Error('Failed to fetch, view log file for JSON errors')
    }

    return res.status(201).json(json?.data || {});
}

You will notice, that after the POST request, we then handle the response data, this is important.

⚠️ IMPORTANT: If you fail to return a response, JSON or string, the Vercel API will fail.

If we have not received a created response, we will through an error to the frontend. If there are errors from the Laravel controller, we then throw a general error and let the user or admin know to check the endpoints log file on Vercel.

Once we have passed all those possibilities, we then return the data to the end user as a Success :)

That would wrap up that part of the code.

3. Creating a Simple HTML form to send data to the API - The Frontend Code

We can make this work with a simple, and un-styled HTML form. This is because the pretty does not equal the working part :)

The file can be created and placed in a components directory at the root of the Next.js application, ./components/ContactUs.js.

The main thing to remember here is to intercept and cancel the Form submit event. i.e. using event.preventDefault() in the handler function that you attach to the <form> element, this prevents it from sending data back to the same page that won't know what to do with the information.

You can try a simple form layout for the frontend:

import React, { useState } from 'react'

let Alert = ({ message, type = 'success'}) => {
  return (
    <div style={{
      display: 'block',
      width: '100%',
      background: type === 'success' ? 'rgba(0, 255, 0, 0.2)' : 'rgba(255, 0, 0, 0.2)',
      color: 'black',
      padding: '1rem',
      margin: '0.5rem',
      borderRadius: '10px',
      boxShadow: '0px 4px 10px rgba(0,0,0,0.1)'
    }}>
      {message ? message : ('The World Needs more `Lerts')}
    </div>
  )
}

const ContactUs = () => {
  const [contactName, setContactName] = useState('')
  const [contactEmail, setContactEmail] = useState('')
  const [honey, setHoney] = useState('')

  /**
   * The error message variable and function
   */
  const [alertBanner, setAlertBanner] = useState(null)

  function handleForm(e) {
    e.preventDefault()

    if (honey.length >= 1) {
      return
    }

    let body = {
      name: contactName,
      email: contactEmail,
      website: honey,
    }

    fetch(
      `${location.origin}/api/contactus`,
      {
        method: 'POST',
        body: JSON.stringify(body)
      }
    )
      .then(response => {

        if (response.status >= 300) {
          setAlertBanner({
            message: response.statusText,
            type: 'error'
          })

          return;
        }

        setAlertBanner({
          message: response.statusText,
          type: 'success'
        })

        console.debug('response')
        console.debug(response);
      })
      .catch(error => {

        setAlertBanner({
          message: response.statusText,
          type: 'error'
        });

        console.debug('error')
        console.error(error)
      })
  }

  return (
    <>
      <form onSubmit={handleForm} id="contactUsForm">
        {/* The Alert Box for the error message */}
        {alertBanner !== null ? <Alert { ...alertBanner } /> : null}

        <div style={{ marginBottom: '0.75rem', display: 'flex', flexDirection: 'column' }}>
          <label htmlFor="name">Name</label>
          <input
            name="name"
            id="name"
            value={contactName}
            onChange={e => setContactName(e.target.value)}
            placeholder="Your Name"
            type="text"
          />
        </div>
        <div style={{ marginBottom: '0.75rem', display: 'flex', flexDirection: 'column' }}>
          <label htmlFor="email">Email</label>
          <input
            name="email"
            id="email"
            value={contactEmail}
            onChange={e => setContactEmail(e.target.value)}
            placeholder="Your email"
            type="email"
          />
        </div>

        <input
          style={{ display: 'none' }}
          name="website"
          value={honey}
          onChange={e => setHoney(e.target.value)}
        />

        <button htmlFor="contactUsForm" type="submit">Submit</button>
      </form>
    </>
  )
}

export default ContactUs;

You can then use this component in a page with by importing the file and then using the <ContactUs /> component.

An example could be in the index page, it would like like this now:

import Head from 'next/head'
import styles from '../styles/Home.module.css'
import ContactUs from '../components/ContactUs'

export default function Home() {
  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>
          Welcome to <a href="https://nextjs.org">Next.js!</a>
        </h1>

        <p className={styles.description}>
          This is an example contact page
        </p>

                {/* Delete the grid area of the index page and place the component there. */}
        <ContactUs />
      </main>

      <footer className={styles.footer}>
        <a
          href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
          target="_blank"
          rel="noopener noreferrer"
        >
          Powered by{' '}
          <img src="/vercel.svg" alt="Vercel Logo" className={styles.logo} />
        </a>
      </footer>
    </div>
  )
}

Create An ENV file for local development

To be able to change the environment variables for your local Next.js app, you can create a .env.local file in the root directory. In that you can place the APP_URL and the APP_TOKEN that you need to use in the actual app.

So you would have the following in the file:

# .env.local

APP_URL="http://localhost:8000"
APP_TOKEN="random_token_that_is_really_long_but_not_short"

4. Trying the system out

Now that you have built all those parts, you should have a file structure that looks similar to the below example:

.
├── components/
│   └── ContactUs.js [New]
└── pages/
    ├── api/
    │   └── contactus.js [New]
    └── index.js [Edited]

We now have a Next.js application with a the form, an API endpoint and the token that you generated in Part 1, now we are going to boot both apps up and test them out.

You can start either of them first, so for this we can run npm run dev or yarn dev as we are in this project already, then go to the Laravel one and run php artisan serve. Also for the Laravel instance you can start up Mailhog or have Mailtrap running.

You can visit each one to make sure they are running.

Local setup for a static form with Vercel

To summarize what is needed to get a version of this running in your local site working, you would need the following basically:

Production / Deployed static form

When you are happy with the changes you have made to the code, you can go about the following for making these available in a live environment other than your computer