Validating the data received from an API request is a critical aspect to consider when building robust and scalable applications. API (Application Programming Interface) acts as an intermediary that enables data to be shared between various software applications. Certifying the data shared through APIs is important as it handles errors and guarantees data integrity and consistency. This tutorial demonstrates the need for API data validation by building a blog application.
API data validation is a way of verifying that the data being exchanged meets the laid down criteria. To explain further, the purpose of API data validation is to confirm that the data sent is in the proper format and that the responses received are as expected.
It is often implemented using various validation libraries and frameworks like Joi, Yup, AJV, and class-validator, which provide proper ways to define validation rules and handle validation errors.
In software development, data validation plays a crucial role and we will discuss its key importance in this section.
Prevents errors: By rigorously inspecting the data sent and the responses received, API data validation drastically reduces the amount of errors in the software.
Protects against malicious attacks: Data validation ensures that the data received adheres to the outlined guidelines, thus protecting the application. The risk of exploitation is reduced by rejecting malicious data that could harm the application.
The pivotal role of API data validation in software development makes its importance obvious.
Before starting this tutorial, ensure you have the following setup:
Enough with the theories, let's dive into practical coding. 🚀
In this guide, we're building a straightforward blog application. Users will be able to register, log in, and perform CRUD operations on blog posts. We'll also delve into customizing the Strapi backend with plugins and controllers.
Setting Up the Project:
blog-app
.blog-app
, create another folder named backend
for your backend code.backend
folder, and run:yarn create strapi-app blog-app --quickstart
After installation, you'll see an output similar to this:
Now let’s setup the admin.
Admin Setup: 1. Access Admin Panel: Open your browser and go to http://localhost:1337/admin. 2. Create Admin Account: Complete the required fields and click the "Let’s Start" button. 3. Dashboard Access: You will then be redirected to the Strapi admin dashboard, which looks like this:
Select Content-Type Builder
by the side nav bar and click on Create new collection type
Next, give the collection type a name in the Display name field. You have the freedom to choose the name for the collection; however, in this guide, we'll refer to the collection as Blog.
Select ADVANCED SETTINGS, uncheck the Draft & publish box, and click on Continue.
1> Strapi has a default setting that enables administrators to preview each content sent, providing them with the ability to review and assess it.
Now, let’s configure the collection to have the following fields:
Create a many-t0-one relation
field with the User (from: users-permissions) collection called user.
Click on Add another field and create a Short Text
field called title.
Lastly, add a Long text
field called post and click Finish.
You should have an output similar to the one below after adding the various fields to the Blog collection type. Hit the Save button at the top right and wait a while for Strapi to restart the server automatically.
After creating the Blog collection, we will have to grant permission to authenticated and public users.
Select Settings on the side nav bar and click on Roles under USERS & PERMISSIONS PLUGIN
Click on Authenticated, click on the Blog accordion, tick the Select all box, and hit Save.
Go back to the Roles page and select Public. Scroll to the bottom of the page, click on the Users-permissions accordion, tick the create box in USER section, and hit Save.
In this article, we will validate API data using an inbuilt Strapi validator called Yup. This article chooses this validator because of its rigorous manner of checking the data. You can opt for any other validator as this approach is not validator-specific.
In this section, we will customize the api/auth/local/register
route by creating a custom plugin. When creating a user, we will configure the User
collection-type to accept only username and password. You can choose to customize it to take in other parameters.
Create a folder in the backend folder called Validation and a file in it called index.js. Add the following code to it.
1// backend/Validation/index.js
2const { yup } = require("@strapi/utils"); //Importing yup
3const { object, string } = yup; //Destructuring object and string from yup
4const UserSchema = object().shape({
5 // Creating userSchema
6 username: string().min(3).required(), // username validation
7 password: string() // password validation
8 .min(6) // password should be minimum 6 characters
9 .required("Please Enter your password") // password is required
10 .matches(
11 /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])(?=.{6,})/,
12 "Must Contain 6 Characters, One Uppercase, One Lowercase, One Number and One Special Case Character"
13 ), // Regex for strong password
14});
15module.exports = {
16 // Exporting UserSchema
17 UserSchema,
18};
Above, we created a schema for the User collection type that checks if the username is a string that contains a minimum of 3 characters and a maximum of 10 characters. We also ensured that the password matches a specified regex expression.
After setting up the user schema, create a folder in src/extensions called users-permissions
, then create a file called strapi-server.js in it. In this file, we will create a plugin for the registration route.
Credits: strapi_v4_user_register_override.js
Open the strapi-server.js
file and add the following code snippet.
1// backend/src/extensions/users-permissions/strapi-server.js
2"use strict";
3const _ = require("lodash");
4const jwt = require("jsonwebtoken");
5const utils = require("@strapi/utils");
6const { UserSchema } = require("../../../Validation"); // Importing UserSchema
7const { sanitize } = utils;
8const bycrypt = require("bcryptjs");
9const { ApplicationError, ValidationError } = utils.errors; //Importing Error Handler
10const sanitizeUser = (user, ctx) => {
11 // Sanitizing user
12 const { auth } = ctx.state;
13 const userSchema = strapi.getModel("plugin::users-permissions.user");
14 return sanitize.contentAPI.output(user, userSchema, { auth });
15};
16module.exports = (plugin) => {
17 // JWT issuer
18 const issue = (payload, jwtOptions = {}) => {
19 _.defaults(jwtOptions, strapi.config.get("plugin.users-permissions.jwt"));
20 return jwt.sign(
21 _.clone(payload.toJSON ? payload.toJSON() : payload),
22 strapi.config.get("plugin.users-permissions.jwtSecret"),
23 jwtOptions
24 );
25 };
26 // Register controller override
27 plugin.controllers.auth.register = async (ctx) => {
28 // Validate user
29 try {
30 const { username, password } = await UserSchema.validate(
31 ctx.request.body, // Validating the request body against UserSchema
32 {
33 stripUnknown: true, // Removing unknown fields
34 abortEarly: false, // Returning all errors
35 }
36 );
37 const lowerUsername = username.toLocaleLowerCase(); // Converting username to lowercase
38 const usernameCheck = await strapi // Checking if username already exists
39 .query("plugin::users-permissions.user")
40 .findOne({
41 where: { username: lowerUsername },
42 });
43 if (usernameCheck)
44 throw new ApplicationError( // Throwing error if username already exists
45 "Username already exists",
46 `Username ${username} already exists in the database`
47 );
48 const hahedPassword = await bycrypt.hash(password, 10); // Hashing password
49 let sanitizedUser;
50 let jwt;
51 await strapi
52 .query("plugin::users-permissions.user")
53 .create({
54 // Creating user
55 data: {
56 username: lowerUsername,
57 password: hahedPassword,
58 role: 1
59 },
60 })
61 .then(async (/** @type {any} */ user) => {
62 sanitizedUser = await sanitizeUser(user, ctx); // Sanitizing user
63 jwt = issue(_.pick(user, ["id"]));
64 });
65 return ctx.send({
66 status: "success",
67 jwt,
68 user: _.omit(sanitizedUser, [
69 // Returning user without password and other fields
70 "email",
71 "provider",
72 "confirmed",
73 "blocked",
74 ]),
75 });
76 } catch (error) {
77 // Handling error
78 if (error.name === "ValidationError")
79 throw new ValidationError("An Error occured", error.errors); // Throwing validation error
80 throw error; // Throwing error
81 }
82 };
83
84 plugin.routes["content-api"].routes.unshift({
85 // Adding route
86 method: "POST",
87 path: "/auth/local/register", // Register route
88 handler: "auth.register",
89 config: {
90 middlewares: ["plugin::users-permissions.rateLimit"],
91 prefix: "",
92 },
93 });
94 return plugin;
95};
In the code we've just explored:
id
as its payload. This token plays a crucial role in managing user sessions and authentication.UserSchema
. This is crucial to ensure that the input received aligns with our expected format and structure.ApplicationError
. This results in a 400 bad request response, accompanied by a relevant message to inform the user.role: 1
indicates that you're creating an 'Authenticated' user.try-catch
block to gracefully handle any exceptions that might arise./api/auth/local/register
route to be handled by the auth.register
controller, thereby linking our back-end logic to a specific endpoint.This approach not only streamlines the user registration process but also integrates important security and validation steps, crucial for any robust web application.
Next, we will create another plugin that will handle a POST
request to /api/auth/local
.
Still in the strapi-server.js file, add the following lines of code:
1// backend/src/extensions/users-permissions/strapi-server.js
2"use strict";
3const _ = require("lodash");
4const jwt = require("jsonwebtoken");
5const utils = require("@strapi/utils");
6const { UserSchema } = require("../../../Validation"); // Importing UserSchema
7const { sanitize } = utils;
8const { ApplicationError, ValidationError } = utils.errors; //Importing Error Handler
9const sanitizeUser = (user, ctx) => {
10 // Sanitizing user
11 const { auth } = ctx.state;
12 console.log(auth);
13 const userSchema = strapi.getModel("plugin::users-permissions.user");
14 return sanitize.contentAPI.output(user, userSchema, { auth });
15};
16module.exports = (plugin) => {
17 // JWT issuer
18 const issue = (payload, jwtOptions = {}) => {
19 _.defaults(jwtOptions, strapi.config.get("plugin.users-permissions.jwt"));
20 return jwt.sign(
21 _.clone(payload.toJSON ? payload.toJSON() : payload),
22 strapi.config.get("plugin.users-permissions.jwtSecret"),
23 jwtOptions
24 );
25 };
26 // Register controller override
27 plugin.controllers.auth.register = async (ctx) => {
28 // The logic for the register route
29 }
30
31 // Login controller override
32 plugin.controllers.auth.callback = async (ctx) => {
33 let sanitizedUser;
34 let jwt;
35 try {
36 const { username, password } = await UserSchema.validate(
37 ctx.request.body, // Validating the request body against UserSchema
38 {
39 stripUnknown: true, // Removing unknown fields
40 abortEarly: false, // Returning all errors
41 }
42 );
43 const lowerUsername = username.toLocaleLowerCase();
44 const user = await strapi // Checking if username exists
45 .query("plugin::users-permissions.user")
46 .findOne({
47 where: { username: lowerUsername },
48 });
49 if (!user)
50 throw new ApplicationError("Username or password does not exists"); // Throwing error if username doesn't exists
51 await bycrypt // Comparing password
52 .compare(password, user.password)
53 .then(async (res) => {
54 if (res) return (sanitizedUser = await sanitizeUser(user, ctx)); // Sanitizing user
55 throw new ApplicationError("Username or password does not exists"); // Throwing error if password doesn't match
56 })
57 .catch((e) => {
58 throw e; // Throwing error
59 });
60 jwt = issue(_.pick(user, ["id"])); // Issuing JWT
61 return ctx.send({
62 status: "success",
63 jwt,
64 user: _.omit(sanitizedUser, [
65 // Returning user without password and other fields
66 "email",
67 "provider",
68 "confirmed",
69 "blocked",
70 ]),
71 });
72 } catch (error) {
73 // Handling error
74 if (error.name === "ValidationError")
75 throw new ValidationError("An Error occured", error.errors); // Throwing validation error
76 throw error; // Throwing error
77 }
78 };
79
80 plugin.routes["content-api"].routes.unshift({
81 // Adding route
82 method: "POST",
83 path: "/auth/local", // Login route
84 handler: "auth.callback",
85 config: {
86 middlewares: ["plugin::users-permissions.rateLimit"],
87 prefix: "",
88 },
89 });
90
91plugin.routes["content-api"].routes.unshift({
92 // Adding route
93 method: "POST",
94 path: "/auth/local/register", // Register route
95 handler: "auth.register",
96 config: {
97 middlewares: ["plugin::users-permissions.rateLimit"],
98 prefix: "",
99 },
100 });
101 return plugin;
102};
From the above lines of code:
/api/auth/local
route.UserSchema
.username
provided exists in the database and we returned an error if it doesn’t.jwt
) along with the sanitizedUser
.Open the index.js file in backend/Validation
and add the following lines of code to create a schema for creating and updating a blog post:
1// backend/Validation/index.js
2const { yup } = require("@strapi/utils"); //Importing yup
3const { object, string, number } = yup; //Destructuring object and string from yup
4
5// Code for UserSchema
6
7const BlogCreateSchema = object().shape({
8 // Creating BlogCreateSchema
9 title: string().min(3).required(), // title validation
10 post: string().min(6).required(), // post validation
11});
12const BlogUpdateSchema = object().shape({
13 // Creating BlogUpdateSchema
14 title: string().min(3).optional(), // title validation
15 post: string().min(6).optional(), // post validation
16});
17module.exports = {
18 // Exporting UserSchema
19 UserSchema,
20 // Exporting BlogCreateSchema
21 BlogCreateSchema,
22 // Exporting BlogUpdateSchema
23 BlogUpdateSchema,
24};
Referring to the added lines of code in the index.js file, we ensured that a title
and a post
content is provided for the BlogCreateSchema
and we made the fields optional for the BlogUpdateSchema
. After creating the various schemas, we exported them along with the UserSchema
that was previously exported.
Following the creation of various schemas, we will create a custom controller for the Blog collection.
In the backend folder, navigate to src/api/blog/controllers
, open the blog.js
file, and replace the current lines of code with the one below:
1"use strict";
2/**
3 * blog controller
4 */
5const { createCoreController } = require("@strapi/strapi").factories;
6const {
7 BlogCreateSchema,
8 BlogGetSchema,
9 BlogUpdateSchema,
10} = require("../../../../Validation"); // Importing BlogCreateSchema and BlogUpdateSchema
11const utils = require("@strapi/utils");
12const { ApplicationError, ValidationError } = utils.errors; //Importing Error Handler
13module.exports = createCoreController("api::blog.blog", ({ strapi }) => ({
14 async create(ctx) {
15 try {
16 const { title, post } = await BlogCreateSchema.validate(
17 ctx.request.body, // Validating the request body against BlogCreateSchema
18 {
19 stripUnknown: true, // Removing unknown fields
20 abortEarly: false, // Returning all errors
21 }
22 );
23 const { id } = ctx.state.user // Getting the id
24 const userCheck = await strapi // Checking if user exists
25 .query("plugin::users-permissions.user")
26 .findOne({
27 where: { id },
28 });
29 if (!userCheck) throw new ApplicationError("User not found"); // Throwing an error if user not found
30 const response = await strapi.query("api::blog.blog").create({
31 // Creating the blog post
32 data: {
33 user: userCheck,
34 title,
35 post,
36 },
37 });
38 return response;
39 } catch (error) {
40 if (error.name === "ValidationError")
41 throw new ValidationError("An Error occured", error.errors); // Throwing validation error
42 throw error;
43 }
44 },
45}));
Here:
request.body
, ensuring it contains the specified parameters.id
stored in the request.title
and the post
content along with other fields in the response.After handling the creation of a blog post, we will handle its update next:
1// backend/src/api/blog/controllers/blog.js
2
3// ...
4
5module.exports = createCoreController("api::blog.blog", ({ strapi }) => ({
6 async create(ctx) {
7 // Create blog handler
8 },
9
10 async update(ctx) {
11 try {
12 const valid = await BlogUpdateSchema.validate(
13 ctx.request.body, // Validating the request body against BlogCreateSchema
14 {
15 stripUnknown: true, // Removing unknown fields
16 abortEarly: false, // Returning all errors
17 }
18 );
19 const { id } = ctx.state.user;
20 const userCheck = await strapi // Checking if user exists
21 .query("plugin::users-permissions.user")
22 .findOne({
23 where: { id },
24 });
25 if (!userCheck) throw new ApplicationError("User not found"); // Throwing an error if user not found
26 ctx.request.body = {
27 data: {
28 ...valid, // Passsing the validated data
29 },
30 };
31 const response = await super.update(ctx);
32 return response;
33 } catch (error) {
34 if (error.name === "ValidationError") {
35 throw new ValidationError("An Error occured", error.errors); // Throwing validation error
36 }
37 throw error;
38 }
39 },
40}));
For the simplicity of this article, we will make use of basic HTML as our frontend, as it is just to demonstrate how API data validation works. You can choose to use any frontend framework of your choice.
This article makes use of a generated html template for the UI as it is just to demonstrate API validation
Create a folder in the root directory called frontend and create the following files in it.
1 ┗ frontend
2 ┃ ┣ auth.js
3 ┃ ┣ edit.html
4 ┃ ┣ edit.js
5 ┃ ┣ index.html
6 ┃ ┣ index.js
7 ┃ ┣ login.html
8 ┃ ┣ new.html
9 ┃ ┣ new.js
10 ┃ ┗ register.html
In this section, we will handle user registration and login functionality.
Open the register.html
page and add the following lines of code to it:
1<!-- frontend/register.html -->
2<!DOCTYPE html>
3<html lang="en">
4 <head>
5 <meta charset="UTF-8" />
6 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7 <title>Blog app</title>
8 <style>
9 body {
10 font-family: Arial, sans-serif;
11 background-color: #f4f4f4;
12 text-align: center;
13 }
14 h1 {
15 color: #333;
16 }
17 .errorMsg {
18 background-color: rgb(49, 7, 7);
19 width: fit-content;
20 margin-bottom: 5px;
21 text-transform: uppercase;
22 border-radius: 5px;
23 padding: 5px;
24 color: rgb(231, 228, 228);
25 display: none;
26 }
27 form {
28 width: 300px;
29 margin: 0 auto;
30 background: #fff;
31 padding: 20px;
32 border-radius: 5px;
33 box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
34 }
35 label {
36 display: block;
37 text-align: left;
38 margin-bottom: 8px;
39 color: #555;
40 }
41 input[type='text'],
42 input[type='password'] {
43 width: 80%;
44 padding: 10px;
45 margin-bottom: 10px;
46 border: 1px solid #ccc;
47 border-radius: 4px;
48 }
49 input[type='submit'] {
50 background-color: #333;
51 color: #fff;
52 border: none;
53 padding: 10px 20px;
54 cursor: pointer;
55 border-radius: 4px;
56 }
57 input[type='submit']:hover {
58 background-color: #555;
59 }
60 </style>
61 </head>
62 <body>
63 <!-- defining the type of request using the class "signup"-->
64 <h1 class="signup">Signup</h1>
65 <form>
66 <div class="errorMsg"></div>
67 <label for="username">Username:</label>
68 <input type="text" id="username" name="username" required />
69 <label for="password">Password:</label>
70 <input type="password" id="password" name="password" required />
71 <input type="submit" value="Signup" />
72 </form>
73 <!-- Adding the javasript file -->
74 <script src="./auth.js"></script>
75 </body>
76</html>
Next, open the login.html file and add the following:
1<!-- frontend/login.html -->
2<!DOCTYPE html>
3<html lang="en">
4 <head>
5 <meta charset="UTF-8" />
6 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7 <title>Blog app</title>
8 <style>
9 body {
10 font-family: Arial, sans-serif;
11 background-color: #f4f4f4;
12 text-align: center;
13 }
14 h1 {
15 color: #333;
16 }
17 .errorMsg {
18 background-color: rgb(49, 7, 7);
19 width: fit-content;
20 margin-bottom: 5px;
21 text-transform: uppercase;
22 border-radius: 5px;
23 padding: 5px;
24 color: rgb(231, 228, 228);
25 display: none;
26 }
27 form {
28 width: 300px;
29 margin: 0 auto;
30 background: #fff;
31 padding: 20px;
32 border-radius: 5px;
33 box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
34 }
35 label {
36 display: block;
37 text-align: left;
38 margin-bottom: 8px;
39 color: #555;
40 }
41 input[type='text'],
42 input[type='password'] {
43 width: 80%;
44 padding: 10px;
45 margin-bottom: 10px;
46 border: 1px solid #ccc;
47 border-radius: 4px;
48 }
49 input[type='submit'] {
50 background-color: #333;
51 color: #fff;
52 border: none;
53 padding: 10px 20px;
54 cursor: pointer;
55 border-radius: 4px;
56 }
57 input[type='submit']:hover {
58 background-color: #555;
59 }
60 </style>
61 </head>
62 <body>
63 <h1>Login</h1>
64 <form>
65 <div class="errorMsg"></div>
66 <label for="username">Username:</label>
67 <input type="text" id="username" name="username" required />
68 <label for="password">Password:</label>
69 <input type="password" id="password" name="password" required />
70 <input type="submit" value="Login" />
71 </form>
72 <!-- Adding the javasript file -->
73 <script src="./auth.js"></script>
74 </body>
75</html>
In the login.js
and the register.js
file, we added the auth.js
file as the external JavaScript file. Now, let’s handle authentication/authorization
in the auth.js
file.
1// frontend/auth.js
2const form = document.querySelector('form'); // Getting the form
3const errorElement = document.querySelector('.errorMsg'); // Getting the error element
4const signup = document.querySelector('.signup'); // Getting the signup element which will be used to determine if the user is signing up or logging in
5form.addEventListener('submit', async e => {
6 // Adding an event listener to the form
7 e.preventDefault();
8 const formData = new FormData(form);
9 const username = formData.get('username');
10 const password = formData.get('password');
11 const message = { username, password }; // Creating the data object
12 const url = signup ? '/register' : ''; // Determining the url based on the signup variable
13 await fetch(`http://localhost:1337/api/auth/local${url}`, {
14 // Making the request
15 method: 'POST',
16 body: JSON.stringify(message),
17 headers: {
18 'content-type': 'application/json'
19 }
20 })
21 .then(async e => {
22 const { error, jwt } = await e.json();
23 if (error) {
24 let errorMsg = '';
25 if (error.name === 'ValidationError') {
26 // Checking if the error is a validation error
27 error?.details?.map(err => {
28 errorMsg += `${err}. <br/>`;
29 });
30 }
31 if (error.name === 'ApplicationError') {
32 // Checking if the error is an application error
33 errorMsg = error.message;
34 }
35 errorElement.style.display = 'block';
36 setTimeout(() => {
37 // Hiding the error message after 10 seconds
38 errorElement.style.display = 'none';
39 }, 10000);
40 return (errorElement.innerHTML = errorMsg); // Displaying the error message
41 }
42 localStorage.setItem('jwt', jwt); // Storing the jwt in localStorage
43 window.location.href = '/frontend/index.html'; // Redirecting the user to the index page
44 })
45 .catch(e => {
46 console.log(e.message);
47 });
48});
In the auth.js file:
submit
action.signup
, which will be used to determine the URL for the registration and login action. Next, we made the URL to contain /register
if the signup
class is found. The login.html doesn’t have the class signup
, hence its URL will not contain /register
.form
and passed it as the request body to the POST
request.POST
request was made.jwt
received from the response to the local storage and then redirected the users to the index.html file.In the index.html file, add the following:
1<!-- // frontend/index.html -->
2<!DOCTYPE html>
3<html lang="en">
4 <head>
5 <meta charset="UTF-8" />
6 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7 <title>Blog Posts</title>
8 <style>
9 body {
10 font-family: Arial, sans-serif;
11 background-color: #f4f4f4;
12 text-align: center;
13 color: #333;
14 }
15 h1 {
16 color: #555;
17 }
18 .blog-post {
19 background-color: #fff;
20 border: 1px solid #ccc;
21 padding: 20px;
22 margin: 10px 0;
23 border-radius: 4px;
24 box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
25 text-align: left;
26 }
27 .blog-title {
28 font-size: 24px;
29 font-weight: bold;
30 margin-bottom: 10px;
31 }
32 .blog-content {
33 font-size: 16px;
34 line-height: 1.4;
35 }
36 .button-container {
37 margin-top: 20px;
38 }
39 .button {
40 background-color: #333;
41 color: #fff;
42 border: none;
43 padding: 10px 20px;
44 margin-right: 10px;
45 border-radius: 4px;
46 cursor: pointer;
47 }
48 .edit-button {
49 background-color: #555;
50 }
51 </style>
52 </head>
53 <body>
54 <h1>Blog Posts</h1>
55 <div class="button-container">
56 <button class="button" id="createButton">Create New Blog</button>
57 </div>
58 <div id="blogList"></div>
59 <script src="./index.js"></script>
60 </body>
61</html>
Open the index.js to fetch and display all the blog posts:
1// frontend/index.js
2// Retrieve the JWT token from localStorage
3const jwt = localStorage.getItem('jwt');
4// Ensure the token is available
5if (!jwt) {
6 console.error('JWT token not found in localStorage. Please login first.');
7 window.location.href = '/frontend/login.html';
8} else {
9 // Fetch the blog posts using the token for authentication
10 fetch('http://localhost:1337/api/blogs', {
11 method: 'GET',
12 headers: {
13 Authorization: `Bearer ${jwt}`
14 }
15 })
16 .then(response => {
17 if (!response.ok) {
18 if (response.status == '401' || response.status == '403') {
19 alert('Unauthorized');
20 localStorage.setItem('jwt', '');
21 window.location.href = '/frontend/login.html';
22 }
23 throw new Error(`HTTP Error! Status: ${response.status}`);
24 }
25 return response.json();
26 })
27 .then(({ data }) => {
28 const blogList = document.getElementById('blogList');
29 // Looping through the blog posts
30 data.forEach(({ attributes, id }) => {
31 // Creating and displaying the blog post elements
32 const blogPostDiv = document.createElement('div');
33 blogPostDiv.classList.add('blog-post');
34 const titleElement = document.createElement('h2');
35 titleElement.classList.add('blog-title');
36 titleElement.textContent = attributes.title;
37 const contentElement = document.createElement('p');
38 contentElement.classList.add('blog-content');
39 contentElement.textContent = attributes.post;
40 const editButton = document.createElement('button');
41 editButton.classList.add('button', 'edit-button');
42 editButton.textContent = 'Edit';
43 editButton.addEventListener('click', () => {
44 // Redirecting the user to the edit page passing the blog post id as a query parameter
45 window.location.href = '/frontend/edit.html?id=' + id;
46 });
47 blogPostDiv.appendChild(titleElement);
48 blogPostDiv.appendChild(contentElement);
49 blogPostDiv.appendChild(editButton);
50 blogList.appendChild(blogPostDiv);
51 });
52 })
53 .catch(error => {
54 console.error('Fetch error:', error);
55 });
56}
57// Create New Blog button logic
58const createButton = document.getElementById('createButton');
59createButton.addEventListener('click', () => {
60 // Redirecting the user to the page for creating a new blog post
61 window.location.href = '/frontend/new.html';
62});
We did the following in the index.js file:
jwt
is available else we will redirect users to the login page.jwt
in the local storage.401
or a 403
error, we will redirect the user to the login page else we will display the blog posts.Create New Blog
button that redirects users to the new.html page when clicked on.id
of the blog post as query parameter.After adding a button to each blog post on the index page, we will get the id
from the query and make a get request to Strapi CMS.
Open the edit.html file and add:
1<!-- frontend/edit.html -->
2<!DOCTYPE html>
3<html lang="en">
4 <head>
5 <meta charset="UTF-8" />
6 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7 <title>Blog app</title>
8 <style>
9 body {
10 font-family: Arial, sans-serif;
11 background-color: #f4f4f4;
12 text-align: center;
13 }
14 h1 {
15 color: #333;
16 }
17 .errorMsg {
18 background-color: rgb(49, 7, 7);
19 width: fit-content;
20 margin-bottom: 5px;
21 text-transform: uppercase;
22 border-radius: 5px;
23 padding: 5px;
24 color: rgb(231, 228, 228);
25 display: none;
26 }
27 form {
28 width: 300px;
29 margin: 0 auto;
30 background: #fff;
31 padding: 20px;
32 border-radius: 5px;
33 box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
34 }
35 label {
36 display: block;
37 text-align: left;
38 margin-bottom: 8px;
39 color: #555;
40 }
41 input[type='text'],
42 textarea {
43 width: 80%;
44 padding: 10px;
45 margin-bottom: 10px;
46 border: 1px solid #ccc;
47 border-radius: 4px;
48 }
49 input[type='submit'] {
50 background-color: #333;
51 color: #fff;
52 border: none;
53 padding: 10px 20px;
54 cursor: pointer;
55 border-radius: 4px;
56 }
57 input[type='submit']:hover {
58 background-color: #555;
59 }
60 </style>
61 </head>
62 <body>
63 <h1>Edit Blog</h1>
64 <form>
65 <div class="errorMsg"></div>
66 <label for="title">Title:</label>
67 <input type="text" id="title" name="title" required />
68 <label for="post">Post:</label>
69 <textarea id="post" name="post" rows="4" required></textarea>
70 <input type="submit" value="Save" />
71 </form>
72 <script src="./edit.js"></script>
73 </body>
74</html>
In the edit.js add:
1// frontend/edit.js
2
3// Retrieve the JWT token from localStorage
4const jwt = localStorage.getItem('jwt');
5// Ensure the token is available
6if (!jwt) {
7 window.location.href = '/frontend/login.html';
8} else {
9 // Getting the id of the blog post from the query
10 const queryString = window.location.search;
11 const urlParams = new URLSearchParams(queryString);
12 const id = urlParams.get('id');
13 if (!id) {
14 window.location.href = '/frontend/index.html'; // Redirecting users to the index.html if the id is not found
15 }
16 // Fetch the blog posts using the token for authentication
17 fetch('http://localhost:1337/api/blogs/' + id, {
18 method: 'GET',
19 headers: {
20 Authorization: `Bearer ${jwt}`
21 }
22 })
23 .then(response => {
24 if (!response.ok) {
25 if (response.status == '401' || response.status == '403') {
26 alert('Unauthorized');
27 localStorage.setItem('jwt', '');
28 window.location.href = '/frontend/login.html';
29 }
30 throw new Error(`HTTP Error! Status: ${response.status}`);
31 }
32 return response.json();
33 })
34 .then(({ data }) => {
35 // Displaying the current title and content for the blog post with the id
36 const title = document.getElementById('title');
37 const post = document.getElementById('post');
38 title.value = data.attributes.title;
39 post.value = data.attributes.post;
40 })
41 .catch(error => {
42 console.error('Fetch error:', error);
43 });
44}
45const form = document.querySelector('form');
46const errorElement = document.querySelector('.errorMsg');
47form.addEventListener('submit', async e => {
48 // Adding an event listener to the form
49 e.preventDefault();
50 const formData = new FormData(form);
51 const title = formData.get('title');
52 const post = formData.get('post');
53 const message = { title, post }; // Creating the data object
54 await fetch(`http://localhost:1337/api/blogs/${id}`, {
55 // Making the request
56 method: 'PUT',
57 body: JSON.stringify(message),
58 headers: {
59 'content-type': 'application/json',
60 Authorization: `Bearer ${jwt}`
61 }
62 })
63 .then(async e => {
64 const { data, error } = await e.json();
65 console.log(error);
66 if (error) {
67 let errorMsg = '';
68 // Checking if the error is a validation error
69 if (error.name === 'ValidationError') {
70 error?.details?.map(err => {
71 errorMsg += `${err}. <br/>`;
72 });
73 }
74 // Checking if the error is an application error
75 if (error.name === 'ApplicationError') {
76 errorMsg = error.message;
77 }
78 if (
79 error.name === 'UnauthorizedError' ||
80 error.name === 'ForbiddenError'
81 ) {
82 alert('Unauthorized');
83 localStorage.setItem('jwt', '');
84 window.location.href = '/frontend/login.html';
85 }
86 errorElement.style.display = 'block';
87 setTimeout(() => {
88 // Hiding the error message after 10 seconds
89 errorElement.style.display = 'none';
90 }, 10000);
91 return (errorElement.innerHTML = errorMsg);
92 }
93 window.location.href = '/frontend/index.html'; // Redirecting the user to the index page
94 })
95 .catch(e => {
96 console.log(e.message);
97 });
98});
Here:
id
in the query and redirected the user to the index page if the id or the blog post is not foundPUT
request with the newly updated title
and post
as request body and then redirected the user to the index page.Lastly, we will handle the creation of a new blog post. Add the following to the new.html file:
1<!-- frontend/new.html -->
2<!DOCTYPE html>
3<html lang="en">
4 <head>
5 <meta charset="UTF-8" />
6 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7 <title>Blog app</title>
8 <style>
9 body {
10 font-family: Arial, sans-serif;
11 background-color: #f4f4f4;
12 text-align: center;
13 }
14 h1 {
15 color: #333;
16 }
17 .errorMsg {
18 background-color: rgb(49, 7, 7);
19 width: fit-content;
20 margin-bottom: 5px;
21 text-transform: uppercase;
22 border-radius: 5px;
23 padding: 5px;
24 color: rgb(231, 228, 228);
25 display: none;
26 }
27 form {
28 width: 300px;
29 margin: 0 auto;
30 background: #fff;
31 padding: 20px;
32 border-radius: 5px;
33 box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
34 }
35 label {
36 display: block;
37 text-align: left;
38 margin-bottom: 8px;
39 color: #555;
40 }
41 input[type='text'],
42 textarea {
43 width: 80%;
44 padding: 10px;
45 margin-bottom: 10px;
46 border: 1px solid #ccc;
47 border-radius: 4px;
48 }
49 input[type='submit'] {
50 background-color: #333;
51 color: #fff;
52 border: none;
53 padding: 10px 20px;
54 cursor: pointer;
55 border-radius: 4px;
56 }
57 input[type='submit']:hover {
58 background-color: #555;
59 }
60 </style>
61 </head>
62 <body>
63 <h1>Edit Blog</h1>
64 <form>
65 <div class="errorMsg"></div>
66 <label for="title">Title:</label>
67 <input type="text" id="title" name="title" required />
68 <label for="post">Post:</label>
69 <textarea id="post" name="post" rows="4" required></textarea>
70 <input type="submit" value="Save" />
71 </form>
72 <script src="./new.js"></script>
73 </body>
74</html>
We add the functionality to the new.js file
1// frontend/new.js
2// Retrieve the JWT token from localStorage
3const jwt = localStorage.getItem('jwt');
4// Ensure the token is available
5if (!jwt) {
6 alert('Unauthorized');
7 window.location.href = '/frontend/login.html';
8}
9const form = document.querySelector('form');
10const errorElement = document.querySelector('.errorMsg');
11form.addEventListener('submit', async e => {
12 // Adding an event listener to the form
13 e.preventDefault();
14 const formData = new FormData(form);
15 const title = formData.get('title');
16 const post = formData.get('post');
17 const message = { title, post }; // Creating the data object
18 await fetch(`http://localhost:1337/api/blogs`, {
19 method: 'POST',
20 body: JSON.stringify(message),
21 headers: {
22 'content-type': 'application/json'
23 }
24 })
25 .then(async e => {
26 const { data, error } = await e.json();
27 console.log(error);
28 if (error) {
29 let errorMsg = '';
30 // Checking if the error is a validation error
31 if (error.name === 'ValidationError') {
32 error?.details?.map(err => {
33 errorMsg += `${err}. <br/>`;
34 });
35 }
36 // Checking if the error is an application error
37 if (error.name === 'ApplicationError') {
38 errorMsg = error.message;
39 }
40 if (
41 error.name === 'UnauthorizedError' ||
42 error.name === 'ForbiddenError'
43 ) {
44 alert('Unauthorized');
45 localStorage.setItem('jwt', '');
46 window.location.href = '/frontend/login.html';
47 }
48 errorElement.style.display = 'block';
49 setTimeout(() => {
50 // Hiding the error message after 10 seconds
51 errorElement.style.display = 'none';
52 }, 10000);
53 return (errorElement.innerHTML = errorMsg);
54 }
55 window.location.href = '/frontend/index.html'; // Redirecting the user to the index page
56 })
57 .catch(e => {
58 console.log(e.message);
59 });
60});
Congratulations on reaching the end of this tutorial! 🎉 Throughout this journey, you've learned how to customize the User collection type in Strapi and develop a plugin for managing blog post updates and creations. It's exciting to see what can be achieved with Strapi, isn't it? 😍 If you encounter any issues along the way, please don't hesitate to mention them in the comment section. We're here to help and will promptly address any concerns or errors you may have faced.
Full Stack Web Developer. Loves JavaScript.