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.
What is API data validation?
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.
Importance of API data validation
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.
- Enhances long-lasting performance: Preventing errors and safeguarding against malicious attacks, API data validation enhances the application's performance and guarantees a durable application.
The pivotal role of API data validation in software development makes its importance obvious.
Prerequisites
Before starting this tutorial, ensure you have the following setup:
- Node.js: Use Maintenance or LTS versions (v14, v16, or v18).
- For Strapi v4.3.9 and later, use Node v18.x.
- For Strapi v4.0.x to v4.3.8, use Node v16.x.
- Package Manager: Yarn is recommended. Install it globally with npm i -g yarn.
- NodeJS Knowledge: A basic understanding is required.
- Code Editor: Visual Studio Code is recommended.
Getting Started
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:
- Create a Project Folder: Name it as you like. For this tutorial, we'll use
blog-app
. - Prepare the Backend Directory: Inside
blog-app
, create another folder namedbackend
for your backend code. - Install Strapi: Open your terminal, navigate to the
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:
Setting up Strapi
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.
Creating User API data validation
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.
Validating the User signup route
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:
- We started by defining a function that 'sanctifies' user details. This function is designed to return user information while ensuring sensitive fields like passwords are excluded for security.
- Then, we generated a JSON Web Token (JWT), embedding the user's
id
as its payload. This token plays a crucial role in managing user sessions and authentication. - We also included a step where the request body is validated against the
UserSchema
. This is crucial to ensure that the input received aligns with our expected format and structure. - In cases where the username is already present in our database, our code is set up to trigger an
ApplicationError
. This results in a 400 bad request response, accompanied by a relevant message to inform the user. - Conversely, if the username is new to our database, the user is created. In response, we send back a success message, the generated JWT, and the user details (minus any sensitive information).
- It's important to note the role specification when creating a user. Setting
role: 1
indicates that you're creating an 'Authenticated' user. - Errors are an inevitable part of any process, so we've wrapped our logic in a
try-catch
block to gracefully handle any exceptions that might arise. - Finally, we wrapped up by configuring the
/api/auth/local/register
route to be handled by theauth.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.
Validating the User login route
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:
- A plugin was added to handle requests for the
/api/auth/local
route. - We validated the body of the request body against the
UserSchema
. - Next, we checked if the
username
provided exists in the database and we returned an error if it doesn’t. - After that, we compared the password provided with the password in the database and we returned an error if it didn’t match.
- Lastly, we returned the JSON Web Token (
jwt
) along with thesanitizedUser
.
Creating Blog API data validation
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.
Creating a Blog post
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:
- We validated the
request.body
, ensuring it contains the specified parameters. - Next, we checked for the user using the
id
stored in the request. - We created a blog post and returned its
title
and thepost
content along with other fields in the response.
Updating a Blog post
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}));
Setting up the frontend
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
Registration and Login page
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:
- We got the form from the HTML file and added an event listener that listens for a
submit
action. - In the register.html file, we added a class called
signup
, which will be used to determine the URL for the registration and login action. Next, we made the URL to contain/register
if thesignup
class is found. The login.html doesn’t have the classsignup
, hence its URL will not contain/register
. - In the event listener, we got the username and password from the
form
and passed it as the request body to thePOST
request. - We display any errors that occurred when the
POST
request was made. - Lastly, we stored the
jwt
received from the response to the local storage and then redirected the users to the index.html file.
Displaying the blog post
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:
- We ensured that the
jwt
is available else we will redirect users to the login page. - Next, we fetched all the blog posts using the
jwt
in the local storage. - If we get a
401
or a403
error, we will redirect the user to the login page else we will display the blog posts. - In the index.html page, we added a
Create New Blog
button that redirects users to the new.html page when clicked on. - Each blog post has an edit button that redirects users to the edit.html page, passing the
id
of the blog post as query parameter.
Editing a blog post
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:
- We fetched the blog post from Strapi using the
id
in the query and redirected the user to the index page if the id or the blog post is not found - We made a
PUT
request with the newly updatedtitle
andpost
as request body and then redirected the user to the index page.
Creating a blog post
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});
Conclusion
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.
Resources
Full Stack Web Developer. Loves JavaScript.