How to Solve Sudoku Puzzles Using an API: A Step-by-Step Guide (Part-2)

•

Fri Apr 07 2023

•

17 min read

The previous section of the guide demonstrated the process of utilizing an API to solve Sudoku puzzles.

This involved generating an HTML file containing a Sudoku grid and a Solve button, with the code extracting the grid values and forwarding them to an API endpoint. Upon receiving the response data, the code solved the puzzle and populated the grid with the solution. Additionally, the guide covered how to style the grid with CSS.

Moving forward, this section of the guide will tackle a few minor issues that need to be addressed. Moreover, it will guide the building of a secure mini backend in Node.js that can be utilized to store API keys. You can optimize your Sudoku-solving process by following the steps in this guide.

Step 1:

Storing the API key in the code is not recommended due to security concerns. Saving it on GitHub can lead to unauthorized access, theft, and unauthorized usage, which may result in unexpected charges, especially if it's a paid API. To avoid this, do not share or upload your API key. Instead, consider hiding it using process en visa by creating a .env file to store it.

Let's create a .env file and name it Rapid_API_Key. Then, I will remove the key from the app.js file and store it in the new file. I have developed a small backend to ensure secure API key storage. Nevertheless, it is necessary to acquire the package for this to function properly.

To create a backend, the first step would be to open the terminal and execute the following command:

sh
npm init

This command initializes a new Node.js project in the current directory. It prompts you to provide some basic information, such as the package name, version, description, entry point, test command, and other details required for the project.

After running the above command, a package.json file is generated in the current directory with default values for various fields. You can use this file to manage dependencies and scripts for your Node.js project.

If you do not have npm installed on your system, follow the step-by-step guide on this link to download and install it.

Step 2:

Once you have created the package.json file, you can create a new JavaScript file at the same level as the others. Let's name it "server.js", as this file will contain the backend code.

To define the backend, you can start by setting the Port variable to 8000 using the const keyword. Additionally, you will need to install some packages for the backend to work properly.

You can install the required packages using the following command in the terminal:

sh
npm i axios express cors dotenv

This command installs the axios, express, cors, and dotenv packages as dependencies for your project. The axios package is used for making HTTP requests, express is a popular Node.js framework for building web applications, cors is a middleware package used for enabling cross-origin resource sharing, and dotenv is used for loading environment variables from a .env file.

swift
{
"name": "sudoku-solver",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^0.24.0", // Promise-based HTTP client for Node.js
"cors": "^2.8.0", // CORS middleware for Express.js
"dotenv": "1.10.0", // Load environment variables from .env file
"express": "^4.17.1" // Fast, unopinionated, minimalist web framework for Node.js
}
}

Step 3:

Let's revisit the server.jsfile and create a fundamental server with the widely used Express.js framework in Node.js. Initially, it creates a constant variable called PORT to indicate the port number where the server will be ready to accept incoming requests.

After setting up the PORT variable, the server.js file imports the Axios library, offering a straightforward approach to sending HTTP requests to other servers. Moreover, it imports the CORS middleware to allow cross-origin resource sharing. Lastly, it uses the dotenv library to load environmental variables from a .env file.

The express() function is then used to create a new instance of an Express.js application. Two middleware functions are registered with app.use(). The first one is cors() which allows cross-origin requests, and the second is express.json() which enables parsing of incoming request bodies as JSON.

Finally, the app.listen() function starts the server and listens for incoming requests on the specified port. When the server starts, it logs a message to the console indicating it listens on the specified port.

Overall, this code sets up a basic Express.js server with some middleware functions allowing it to receive and respond to HTTP requests.

js
const PORT = 8000;
const axios = require('axios').default;
const express = require('express');
const cors = require('cors');
require('dotenv').config();
const app = express();
app.use(cors());
app.use(express.json());
app.listen(PORT, () => console.log(`Server listening on port ${PORT}`));

You must include a script in the package.json file to run the code. This can be done by adding two scripts, start:backend and test, to the file. The start:backend script starts the project's backend by running nodemon on the server.js file.

On the other hand, the test script displays an error message if no test is specified.

swift
{
"description": "",
"main": "app.js",
"scripts": {
"start:backend": "nodemon server.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^0.24.0",
"cors": "^2.8.5",
"dotenv": "^10.0.0",
"express": "^4.17.1",
"nodemon": "^2.0.15"
}
}

To install the nodemon package, you need to run the following command in the terminal:

sh
npm i nodemon

Once you have installed the package, you can start the backend by running the start:backend script using the following command in the terminal:

sh
npm run start:backend

After running the command, you should see a message that the server is listening on port 8000. Now you can continue with the project.

Step 4:

Next, I will set up routing to avoid having to pass the API key in the headers when visiting the URL. Instead, I will handle everything on the backend. To do this, I will create a route for an HTTP POST request on the server's /solve endpoint. When a POST request is made to this endpoint, the function that is passed as the second argument (req, res) => {...} will be executed.

Inside this function, you can specify what the server should do when it receives a POST request on the /solve endpoint. You can access the data sent in the request using the req object and the res object to send a response back to the client.

In this case, the function is empty and does not define any behavior for the server. It just contains an empty object literal {}. Here we will add the code inside this function to implement the behavior you want the server to have when it receives a POST request on the /solve endpoint.

js
const PORT = 8000;
const axios = require('axios').default;
const express = require('express');
const cors = require('cors');
require('dotenv').config();
const app = express();
app.use(cors());
app.use(express.json());
app.post('/solve', (req, res) => {
// implementation code goes here
});
app.listen(PORT, () => console.log(`server listening on PORT ${PORT}`));

Step 5:

In this step, I'll simply paste the entire code we copied from the server.js file. However, currently, the data is not being transmitted. Additionally, I'll make minor changes to the ' axios' code block and include res.json to display the response.data.

I will re-enter the fake numbers to ensure that everything works as intended and that the key is correctly identified. This will be hardcoded for now.

js
const axios = require('axios').default;
const express = require('express');
const cors = require('cors');
require('dotenv').config();
const app = express();
app.use(cors());
app.use(express.json());
app.post('/solve', (req, res) => {
const options = {
method: 'POST',
url: 'https://solve-sudoku.p.rapidapi.com/',
headers: {
'content-type': 'application/json',
'x-rapidapi-host': 'solve-sudoku.p.rapidapi.com',
'x-rapidapi-key': process.env.RAPID_API_KEY,
},
data: {
puzzle:
'12.....62....1......6....8...3......9...7....6....4...4.....8....1',
},
};
axios
.request(options)
.then((response) => {
console.log(response.data);
res.json(response.data);
})
.catch((error) => {
console.error(error);
});
});
app.listen(PORT, () => console.log(`server listening on PORT ${PORT}`))

If you navigate to http://localhost:8000/solve in your browser, you won't see any visual output. However, the server generates a response, indicating it's successfully solving the puzzle.

Step 6:

We will use the fetch() method in the app.js file to proceed. By using this method, a POST request is made to a local server at http://localhost:8000/solve. The request includes a JSON payload and specifies that the server should respond with JSON data.

We use the then() method to handle the response from the server. Firstly, we parse the JSON data using the json() method, and then log it to the console with console.log().

The catch() method handles any errors that may occur during the request and logs an error message to the console using console.error(). The code sends a request to a server and logs the response or any errors in the process.

js
const puzzleBoard = document.querySelector('#puzzle');
const solveButton = document.querySelector('#solve-button');
const solutionDisplay = document.querySelector('#solution');
const squares = 81;
const submission = [];
for (let i = 0; i < squares; i++) {
const inputElement = document.createElement('input');
inputElement.setAttribute('type', 'number');
inputElement.setAttribute('min', 1);
inputElement.setAttribute('max', 9);
if ((i % 9 === 0 || i % 9 == 1 || i % 9 == 2) && i < 21 ||
(i % 9 === 6 || i % 9 == 7 || i % 9 == 8) && i < 27 ||
(i % 9 === 3 || i % 9 == 4 || i % 9 == 5) && i < 27 && i < 53 ||
(i % 9 === 0 || i % 9 == 1 || i % 9 == 2) && i < 53 ||
(i % 9 === 6 || i % 9 == 7 || i % 9 == 8) && i < 53) {
inputElement.classList.add('odd-section');
}
puzzleBoard.appendChild(inputElement);
}
const joinValues = () => {
const inputs = document.querySelectorAll('input');
submission.length = 0; // clear previous values
inputs.forEach(input => {
if (input.value) {
submission.push(input.value);
} else {
submission.push('.');
}
});
console.log(submission);
};
const populateValues = (isSolvable, solution) => {
const inputs = document.querySelectorAll('input');
if (isSolvable && solution) {
inputs.forEach((input, i) => {
input.value = solution[i];
});
solutionDisplay.innerHTML = 'This is the answer';
} else {
solutionDisplay.innerHTML = 'This is not solvable';
}
};
const solve = () => {
joinValues();
const data = submission.join('');
console.log('data', data);
fetch('http://localhost:8000/solve', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
};

To test the application, the user inputs some numbers into the Sudoku grid and clicks the Solve button. The backend server then handles the API request to solve the puzzle and returns the solution to the front end.

The API key is now securely hidden on the backend server and is not exposed on the front end.

Step 7:

Nonetheless, there is a minor issue that we still need hard coding. To resolve this, we must pass parameters between the front and back-end. To achieve this quickly, I will demonstrate the process. In addition to the header, I will also pass a body through which I will transmit some data. To do so, I must enclose the data within JSON.stringify.

The JSON.stringify() method converts the data value to a JSON string that can be sent in the request body. The body property then includes the JSON string in the request body.

js
const puzzleBoard = document.querySelector('#puzzle');
const solveButton = document.querySelector('#solve-button');
const solutionDisplay = document.querySelector('#solution');
const squares = 81;
const submission = [];
// Create input elements for the puzzle board
for (let i = 0; i < squares; i++) {
const inputElement = document.createElement('input');
inputElement.setAttribute('type', 'number');
inputElement.setAttribute('min', 1);
inputElement.setAttribute('max', 9);
// Add odd-section class to input elements that belong to a certain section
if ((i % 9 === 0 || i % 9 == 1 || i % 9 == 2) && i < 21 ||
(i % 9 === 6 || i % 9 == 7 || i % 9 == 8) && i < 27 ||
(i % 9 === 3 || i % 9 == 4 || i % 9 == 5) && i < 27 && i < 53 ||
(i % 9 === 0 || i % 9 == 1 || i % 9 == 2) && i < 53 ||
(i % 9 === 6 || i % 9 == 7 || i % 9 == 8) && i < 53) {
inputElement.classList.add('odd-section');
}
puzzleBoard.appendChild(inputElement);
}
// Function to join input values into a submission array
const joinValues = () => {
const inputs = document.querySelectorAll('input');
submission.length = 0; // clear previous values
inputs.forEach(input => {
if (input.value) {
submission.push(input.value);
} else {
submission.push('.');
}
});
console.log(submission);
};
// Function to populate input values based on the solution
const populateValues = (isSolvable, solution) => {
const inputs = document.querySelectorAll('input');
if (isSolvable && solution) {
inputs.forEach((input, i) => {
input.value = solution[i];
});
solutionDisplay.innerHTML = 'This is the answer';
} else {
solutionDisplay.innerHTML = 'This is not solvable';
}
};
// Function to send a POST request to the server to solve the puzzle
const solve = () => {
joinValues();
const data = { numbers: submission.join('') };
console.log('data', data);
fetch('http://localhost:8000/solve', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
};
solveButton.addEventListener('click', solve);

Step 8:

Let's go to the server.js file and console.log out the request. The console.log('AAA', req) logs the req object to the console and the string AAA. The req object represents the HTTP request to the /solve endpoint.

The AAAstring is likely added to help distinguish this particular log statement from other log statements that may be present in the console output.

js
const axios = require('axios').default;
const express = require('express');
const cors = require('cors');
require('dotenv').config();
const app = express();
app.use(cors());
app.use(express.json());
const PORT = process.env.PORT || 8000;
app.post('/solve', (req, res) => {
console.log('AAA', req);
const options = {
method: 'POST',
url: 'https://solve-sudoku.p.rapidapi.com/',
headers: {
'content-type': 'application/json',
'x-rapidapi-host': 'solve-sudoku.p.rapidapi.com',
'x-rapidapi-key': process.env.RAPID_API_KEY,
},
data: {
puzzle:
'12.....62....1......6....8...3......9...7....6....4...4.....8....1',
},
};
axios
.request(options)
.then((response) => {
console.log(response.data);
res.json(response.data);
})
.catch((error) => {
console.error(error);
});
});
app.listen(PORT, () => console.log(`server listening on PORT ${PORT}`));

Step 9:

The next step is to replace the currently hardcoded string and instead retrieve the puzzle from the request body using req.body.numbers. This will allow the Sudoku puzzle to be obtained from the request body using the req.body.numbers syntax.

js
const axios = require('axios').default;
const express = require('express');
const cors = require('cors');
require('dotenv').config();
const app = express();
app.use(cors());
app.use(express.json());
const PORT = process.env.PORT || 8000;
app.post('/solve', (req, res) => {
const options = {
method: 'POST',
url: 'https://solve-sudoku.p.rapidapi.com/',
headers: {
'content-type': 'application/json',
'x-rapidapi-host': 'solve-sudoku.p.rapidapi.com',
'x-rapidapi-key': process.env.RAPID_API_KEY,
},
data: {
puzzle: req.body.numbers
},
};
axios
.request(options)
.then((response) => {
console.log(response.data);
res.json(response.data);
})
.catch((error) => {
console.error(error);
});
});
app.listen(PORT, () => console.log(`server listening on PORT ${PORT}`));

We also need to add the populateValues function. This function takes two arguments, data.solvable and data.solution, which are properties of the data object returned from the server.

js
const solve = () => {
joinValues();
const data = { numbers: submission.join('') };
console.log('data', data);
fetch('http://localhost:8000/solve', {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(data => {
console.log(data);
populateValues(data.solvable, data.solution);
submission = [];
})
.catch(error => {
console.error('Error:', error);
});
};
solveButton.addEventListener('click', solve);

We need to make a minor modification in the app.js file by replacing the const submission declaration with let submission. Changing const submission to let submission allows us to modify the submission array.

This is important for the functionality of the code because the submission array needs to be updated as the user inputs more numbers, and we need to be able to modify it as the program runs.

js
const puzzleBoard = document.querySelector('#puzzle');
const solveButton = document.querySelector('#solve-button');
const solutionDisplay = document.querySelector('#solution');
const squares = 81;
let submission = [];
// Create input elements for the puzzle board
for (let i = 0; i < squares; i++) {
const inputElement = document.createElement('input');
inputElement.setAttribute('type', 'number');
inputElement.setAttribute('min', 1);
inputElement.setAttribute('max', 9);
// Add odd-section class to input elements that belong to a certain section
if ((i % 9 === 0 || i % 9 == 1 || i % 9 == 2) && i < 21 ||
(i % 9 === 6 || i % 9 == 7 || i % 9 == 8) && i < 27 ||
(i % 9 === 3 || i % 9 == 4 || i % 9 == 5) && i < 27 && i < 53 ||
(i % 9 === 0 || i % 9 == 1 || i % 9 == 2) && i < 53 ||
(i % 9 === 6 || i % 9 == 7 || i % 9 == 8) && i < 53) {
inputElement.classList.add('odd-section');
}
puzzleBoard.appendChild(inputElement);
}
// Function to join input values into a submission array
const joinValues = () => {
const inputs = document.querySelectorAll('input');
submission.length = 0; // clear previous values
inputs.forEach(input => {
if (input.value) {
submission.push(input.value);
} else {
submission.push('.');
}
});
console.log(submission);
};
// Function to populate input values based on the solution
const populateValues = (isSolvable, solution) => {
const inputs = document.querySelectorAll('input');
if (isSolvable && solution) {
inputs.forEach((input, i) => {
input.value = solution[i];
});
solutionDisplay.innerHTML = 'This is the answer';
} else {
solutionDisplay.innerHTML = 'This is not solvable';
}
};
// Function to send a POST request to the server to solve the puzzle
const solve = () => {
joinValues();
const data = { numbers: submission.join('') };
console.log('data', data);
fetch('http://localhost:8000/solve', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
};
solveButton.addEventListener('click', solve);

Let's repeat the previous step and click on the Solve button. The output looks great.

Step 10:

To improve the project's appearance, we'll apply some styling. We can enhance the Solve button by specifying its background color and font color. Furthermore, we can add a 10-pixel padding and 20-pixel margin to it, as well as a border radius to provide a more professional appearance.

css
#puzzle {
width: 450px;
height: 450px;
}
#puzzle input {
width: 50px;
height: 50px;
box-sizing: border-box;
border-spacing: 0;
border: 1px solid grey;
}
.odd-section {
background-color: lightgrey;
}
#solve-button {
background-color: coral;
color: white;
padding: 10px;
margin: 20px;
border-radius: 10px;
border: none;
}
#solve-button:hover {
background-color: #c96f4c;
}

This is the final look of the Sudoku solver web application after implementing the Sudoku solving algorithm and adding styling to the web page.

The Solve button is styled with a custom background color, font color, padding, margin, and border-radius to give the application a polished look. Overall, the web application is fully functional and visually appealing.

Wrap up

​​The project involves building a Sudoku puzzle solver app using JavaScript. Initially, the app used an external API to solve the puzzle, but the API key was exposed on the front end.

To address this, the backend was modified to hide the API key and handle the solving of the puzzle. This involved creating a route for an HTTP POST request and sending a JSON payload to the server. The response from the server was then parsed and displayed on the front end.

The styling of the app was also improved by modifying the solve button. The project's final output was a functional Sudoku puzzle solver app with improved security and aesthetics.