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

Reece May • March 12, 2021 • 9 min read

laravel nextjs react tutorial

We shall consider an interesting way to implement your own way of handling static forms with a Laravel application and a static website such as Next.js or even things like Svelte, Gatsby and the list continues on will continue.

The benefit of this is that the websites can be on separate subdomains, eg. example.com (static) and app.example.com (Laravel).

Random rambling over; and I hope that I possibly have your attention, lets see how to do this 🙃

In Part 1 we are going to cover the setup for the Laravel side of things, then in Part 2 we will see how to get the form sending using Vercel's API functionality.

Requirements for both parts

With regards setup, right now Laravel really offers some great ways to setup a new application on most platforms.

Create a new directory, or folder to hold both your Next.js site and the Laravel site: mkdir static_site_form_laravel && cd static_site_form_laravel or CMD+Shift+N or left click, new folder. Make your way into that directory.

The normal way I create a Laravel app is laravel new static-form-back, it works and really has no place for messing up (it puts what you need right there)

You may need to run npm install if it hasn't run yet by the laravel cmd.

We won't need a database for this, you can add it later as the messages sent can just be sent to the log or sent straight away in a Mailable class.

Setting up the Next.js app will be covered in part 2

You should be set up after running all the install commands. As there aren't any extra things needed.

I may make an example application with both the vercel app and the laravel one together. If people read this...

Setting up your Laravel application to receive static form submissions

You and I are going to build our Laravel side of the application first as this will give us the choice of making it accept a wide range of form submissions, but not be narrow in scope of what it's receiving.

The first things we would need to do is to think of the process flow our data would have:

  1. An API token that identifies the static website
  2. An endpoint is needed first for the data coming in
  3. We need to make sure that source is allowed to submit that data
  4. Validating and handling the incoming data
  5. We send a response depending on what came in, 👍 or 👎

At the end of these, you will have the following new directories/files. Don't make them yet though :)

1.
2└── app/
3 ├── Actions/
4 │ └── CreateStaticToken.php [New, see #1]
5 └── Http/
6 ├── Controllers/
7 │ └── Static/
8 │ └── ContactController.php [New, see #2]
9 └── Middleware/
10 └── Static/
11 └── ValidStaticSiteKey.php [New, see #3]

To begin, we will first flesh out creating our token. It is like the skeleton of this project, without it the app would be pointless.

1. Creating an API token for a server

As there are multiple ways this can be approached, we are going to use the current idea of Actions in the Laravel community. They are enjoyable and convey the idea of what they are doing.

First create a new file called Actions/CreateStaticToken.php.

In there we will place our logic to create a token, this means that as a central place, whenever you change the Algorithm behind the token, you change it in one place.

Actions/CreateStaticToken.php

1<?php
2 
3namespace App\Actions;
4 
5use Illuminate\Support\Facades\Storage;
6use Illuminate\Support\Str;
7 
8class CreateStaticToken
9{
10 public function create()
11 {
12 $tokenHash = hash(
13 'sha256', /** Your hash algorithm, change as you please, but _not_ MD5 !!!*/
14 $textToken = Str::random(40)
15 );
16 
17 // Storing the HASH version,
18 Storage::put('static.token', json_encode([
19 'hash' => $tokenHash,
20 ]));
21 
22 return $textToken;
23 }
24}

Calling this to create a new token would go like this: (new CreateStaticToken)->create()

So this would be called via a CLI command or from a UI and API endpoint inside your App, even with Nova to regenerate the token or create it.

2. Creating Routes

We are going to solve point number 2 and 4 at the same time. Why?? This is because if you define the route in Laravel and give it a controller name, then try to create that controller in Artisan, you will have an error.

So...

Run the following in the terminal of your Laravel project:

1php artisan make:controller StaticForms\\ContactController --invokable

I am being opinionated here with the invokable controller, it helps imply that this is only handling one POST action.

Now that you have a Controller, you can create a route for it in your routes/api.php file

In our file place the following

1// This include would be at the top of your file.
2use App\Http\Controllers\Static\ContactController;
3 
4Route::post('/static/contact', ContactController::class)->name('api.static.contact.create');

Awesome, you've read over 3900 letters, have a break for a second 🙃 and enjoy your coffee or read over that little bit to take it in again.

Onward we go. Or anyone else for that matter. Our route is not very secure right now.

So next, because we don't want random people, script kiddies and the odd bot, having at your form and mail system we need to check that the origin of the data is allowed to cause further action on the server.

3. Middleware for Securing the static form

We are going to make use of a Middleware that will intercept all our incoming requests to our static form endpoints. This makes it easier to group all your routes under a single middleware and add new endpoints as you need them

To make the middleware in Laravel, run the following Artisan command:

1php artisan make:middleware Static\\ValidStaticSiteKey

Open up your new middleware file: app/Http/Middleware/Static/ValidStaticSiteKey.php (if you have source control you should see some new colours in your sidebar)

With the middleware we are going to basically determine if a value sent via the header matches one stored and hashed on the Laravel app.

The design of this is that it uses a token that is similar to an API token, but it is assigned to a specific function or site and not a user.

This means that you as the Admin can create a new token that isn't linked to a user.

The token is stored on the server (or in a S3 like storage instance) and then read from the file and validated if the hash matches.

Below, I present a dump of code for you:

I'll break it down for you though

1<?php
2 
3namespace App\Http\Middleware\Static;
4 
5use Closure;
6use Illuminate\Support\Facades\Storage;
7 
8class ValidStaticSiteKey
9{
10 /**
11 * The static site token.
12 * @var string
13 */
14 protected const TOKEN_HEADER = 'X-Static-Site-Token';
15 
16 /**
17 * Handle an incoming request.
18 *
19 * @param \Illuminate\Http\Request $request
20 * @param \Closure $next
21 * @return mixed
22 */
23 public function handle($request, Closure $next)
24 {
25 if (!$request->hasHeader(self::TOKEN_HEADER)) {
26 
27 return $request->expectsJson()
28 ? response()->json(['error' => 'missing expectation of the greater ostrich'], 422)
29 : response('Good day, there seems to be some missing requirements', 422);
30 }
31 
32 $token = rescue(function () {
33 $token = Storage::get('static.token');
34 $token = json_decode($token, true);
35 
36 return $token['hash'];
37 });
38 
39 $staticToken = hash('sha256', $request->header(self::TOKEN_HEADER));
40 
41 return hash_equals($token, $staticToken)
42 ? $next($request)
43 : response()->json(['error' => 'err::mismatch'], 401);
44 }
45}

Check if the request meets basic requirements

The first part does some pre-checks.

1if (! $request->hasHeader(self::TOKEN_HEADER)) {
2 
3 // Return either a text/html response or application/json response
4 return $request->expectsJson()
5 ? response()->json(['error' => 'missing expectation of the greater ostrich'], 422)
6 : response('Good day, there seems to be some missing requirements', 422);
7}

It checks if the request has the header added if not don't bother letting the request continue. You can add a more standard Missing Requirement message, I opted for an odd one as no one should be calling this endpoint normally.

Retrieve the token

1/**
2 * Using the rescue function makes sure we don't just randomly fail here.
3 */
4$token = rescue(function () {
5 $token = Storage::get('static.token');
6 $token = json_decode($token, true);
7 
8 return $token['token'];
9});

This part is retrieving our token from our storage medium, this instance is file storage. It then decodes the JSON string and returns the hashed text token.

Validate the API token

Once we have the original hash, we calculate the hash of the token sent, then comparing it we determine if the request can continue or if it has failed to equal.

1$staticToken = hash('sha256', $request->header(self::TOKEN_HEADER));
2 
3return hash_equals($token, $staticToken)
4 ? $next($request)
5 : response()->json(['error' => 'err::mismatch'], 401);

Now to apply the Middleware, we are going to wrap our route in a group, this will allow us to have more controllers in the future while all of them have the middleware always applied.

1use App\Http\Middleware\Static\ValidStaticSiteKey;
2 
3Route::middleware(ValidStaticSiteKey::class)
4 ->group(function() {
5 // Our contact controller and route are now here.
6 Route::post('/static/contact', ContactController::class)
7 ->name('api.static.contact.create');
8 // and any other routes.
9 });

Now you can move onto working on validating the data in your controller.

4. Validating and handling the incoming data

Inside our controller App\Http\Controllers\Static\ContactController::class we are going to edit the invokable function.

For this part you can use a custom FormRequest class for validation or, as we are sticking with a simple example you can do the validation in the function.

1public function __invokable(Request $request)
2{
3 $validated = $this->validate($request, [
4 'name' => ['required', 'string'],
5 'email' => ['required', 'email'],
6 'message' => ['sometimes', 'string'],
7 'website' => ['not_regex:/.*/'] // or honeypot, but I find website is bot default
8 ]);
9 
10 $result = rescue(function () use ($validated) {
11 
12 $rawText = sprintf(
13 "Name: %s \nEmail: %s \nMessage: %s\n",
14 $validated['name'],
15 $validated['email'],
16 $validated['message']
17 );
18 
19 // this can be your mailable here, we are just using a raw mail
20 Mail::raw(
21 $rawText,
22 function ($message) {
23 $message->to('you@example.com')
24 ->subject('New Contact');
25 }
26 );
27 });
28 
29 return $result
30 ? response()->json(['message' => 'Success'], 200)
31 : response()->json(['message' => 'Error Sending Message'], 400);
32}

Our validation is simple for this example but can be adjusted to what you need.

Once we have determined that the info is all nice, then you can send an email or persist the data to a DB/Model.

5. Sending the response back via the API

Because most of the time we will be sending it back to our Serverless API, we are going to return JSON.

You will see that the controller responds with a status code and a message in JSON format. Also the Middleware does a similar thing when the header is present otherwise it will send back a HTML or plain text response if the request accepts that.

We always want to give a response we know about and also can parse and decide on what to show to the user.

So basically, so far we have 200, 400, 401 and 422 (500 if something really wrong happens).

We will structure our frontend code and Serverless API code to handle those and display the appropriate message to a human using the form.

So basically, we can tell the user after reading the status code in the API file, 'you have entered the wrong information', 'there was an error sending your message' or 'we have received your mail and should contact you soon'

But, there might be that point of 401, 500 or a 422 for the token. That might be useful to fire a message to your own message service and check out why something is really messed up.

Conclusion and Part 2

Thank you for reading this far and going through this part of the article with me. I will be sharing Part 2 alongside this article at the same time so you can build the static site with the API and form at the same time and also the Laravel application.

To read part two click here Part 2 ->

Some background to why I thought of sharing this

If you have made it this far well done, if you skipped everything and came down here, the repo is not here :)

I have been building a landing page for my one project, the static site is on Vercel and backing it with Prismic for the content and using Next.js for the framework. All in all it is a wicked combination to work with.

For most static sites I have deployed on Netlify, this includes the form handling functionality by default, that is nice. (Like good old static html pages)

But then I wanted to share the details of what I did to make use of Vercel's API functionality to handle static forms and send that data to a Laravel server. In that way removing the need for tokens in the frontend and also allowing for the token to be updated via ENV values and the app re-built without the need of someone seeing the token, I think that is quite cool.

Then the Laravel app has a section that handles the one off token and the middleware to validate the request. After that, you can create whatever controller and endpoint, and handle the incoming data.

The benefits of this is that you don't have to have a reliance on third party services. This is nice as there isn't a limit and you can determine if you want to send an email immediately or if you want to vet it for spam.

.... Random thoughts and rambling that follows :);

I also always laugh at how humans always have a similar thought pattern. You can always expect across the board globally that someone will have a similar idea to you. Don't matter about there qualifications or experience. They thinking and will happen to come up with the same/similar idea. It's annoying sometimes, but it actually proves in a way that you aren't that far off in coming up with ideas or tinkering on something.

Someone may have done it before you or after, but that's cool. Each person conveys information differently, each person consumes that differently. That really gives you greater scope and understanding of information.

But I guess if you made it this far, you probably think I'm a bit off my rocker. But I hope you will read through the next part and build something cool too :).

Please leave a message if you think this was useful or could be improved. (Mails go straight to me, not a mail service thing, what's the point?)

First version on 08 Jan 2021

Syntax highlighting by Torchlight.