DEV Community

Timilehin Okunola
Timilehin Okunola

Posted on

How To Use Superstruct in an Express.js Application

Introduction

Data types are an important part of any programming language. Yet poor data input handling can inject chaos into an application. Hence, data validation is an extremely important aspect of any web application.

Data input validation within the Javascript ecosystem may be daunting. This is where Superstruct comes in. Superstruct is a Javascript library designed to simplify and strengthen data validation.

Superstruct leverages a declarative schema-based approach, allowing you to define your data's structure and constraints precisely. Beyond basic validation, it offers transformation and refinement capabilities.

It is designed for validating data at runtime, so it throws (or returns) detailed runtime errors for the developer to manage or the application end users. This is especially useful in web applications that use REST API or GraphQL API.

In this tutorial, we will create a basic REST API for a blog application using Superstruct for data validation. For simplicity, we won’t use any database connection, and the data will be stored in real-time. We will then go on to test our endpoints using Postman.

Build The Server

Prerequisites

To follow along with this tutorial, you need to have the following installed on your computer;
Node.js
NPM
A code editor. We will be using VS Code for this tutorial.

Also, you should have a basic knowledge of Javascript and Nodejs/Express.

First, create a folder on your computer and open this folder in your code editor.
Open your terminal and initialize a new node project by running the command below in your terminal.

npm init -y
Enter fullscreen mode Exit fullscreen mode

Then install Express and the superstruct in your project by running the commands below.

npm install express

npm install --save superstruct
Enter fullscreen mode Exit fullscreen mode

Next, create an index.js file and paste the following code to create the server.

const express = require("express");

const app = express();
app.use(express.json());

app.get("/", (req, res) => {
    return res.status(200).json({
      message: "Hello World"
    })
  });

  app.listen(5000, () => {
    console.log("Note app listening on port 5000");
  });
Enter fullscreen mode Exit fullscreen mode

We have a basic server set up now. We can start the server by running the command below

node index
Enter fullscreen mode Exit fullscreen mode

Create The Controllers

Create a folder named controllers. Create two new files named AuthController.js and PostController.js.

Paste the code into the AuthController.js file.

const { object, string, number, assert } = require("superstruct");

const UserDetails = object({
  id: number(),
  firstName: string(),
  lastName: string(),
  userName: string(),
});

//Create User
const createUser = async (req, res) => {
  try {
    assert(req.body, UserDetails, “User data is invalid”);
    return res.status(200).json({message:"User Signed Up successfully"})
  } catch (error) {
    const { path, failures } = error;
    // Handle errors based on path and error messages
  const errorMessage = failures()
    console.log("Validation failed:", path, errorMessage);
    return res.status(400).json({Error: ` ${JSON.stringify(errorMessage)}`})
  }
};

const LoginDetails = object({
    userName: string()
})
// Signin User
const loginUser = async (req, res) => {
try {
    assert(req.body, LoginDetails, “User data is invalid”)
    return res.status(200).json({message:"User Signed Up successfully"})
} catch (error) {
    const { path, failures } = error;
    // Handle errors based on path and error messages
  const errorMessage = failures()
    console.log("Validation failed:", path, errorMessage);
    return res.status(400).json({Error: ` ${JSON.stringify(errorMessage)}`})
}
};

module.exports = {
  createUser,
  loginUser,
};
Enter fullscreen mode Exit fullscreen mode

In the above code snippet, first, we imported the object, string, number, and assert methods from the superstruct module.
Next, we declared our user’s details struct and created the createUser function. In the createUser function, we first checked using the assert method if the object passed in the body complies with the types we specified in the Userdetails struct. We also passed in an error explanation should the object being compared with the user details return an error.

This is declared within a try-catch block to catch any error in the checking process. Superstruct makes use of the StructError class to define errors.
Below is an example of what the StructError looks like.

StructError {
  value: false,
  key: 'email',
  type: 'string',
  refinement: undefined,
  path: ['email'],
  branch: [{ id: 1, name: 'Alex', email: false }, false],
  explanation: "An invalid email was passed"
  failures: [Function]
}
Enter fullscreen mode Exit fullscreen mode

The failure returns an array of multiple StructError instances, each explaining each error. If only one error occurred, it returns an array containing an instance of just one StructError describing the error.

In the catch block, simply get the path class and failures method from the thrown error. Then, we call the failures methods to get the StructError instance(s) array. Then, we log the path and the errorMessages to the console. We also send a response status of 400 and include the JSON format of the errorMessages in the json response.

In the loginUser function, we replicated what we did with the createUser function but declared a new struct called LoginDetails containing just the userName.

To create our post controller, paste the code below into the PostController.js file

const { object, string, number, assert, enums, optional } = require("superstruct");

const PostDetails = object({
    id: number(),
    title: string(),
    content: string(),
    category: optional(enums(["Sports", "Technology", "Business", "Religion"])),
  });

//
const createPost = async(req, res) => {
try {
    assert(req.body, PostDetails, “Post data is invalid”);
    return res.status(200).json({message:"Post Created Up successfully"})
} catch (error) {
    const { path, failures } = error;
    // Handle errors based on path and error messages
  const errorMessage = failures()
    console.log("Validation failed:", path, errorMessage);
    return res.status(400).json({Error: ` ${JSON.stringify(errorMessage)}`})
}
}

module.exports = {
  createPost
};

Enter fullscreen mode Exit fullscreen mode

In the above snippet, we also did something similar to what with the authController. However, we implemented the optional and enums method here.

We have successfully created the controllers.

Create The Routes

Now, let us create our routes. First, create a new folder named routes. In the routes folder, create two files named auth.js and post.js.
Paste the code below in the auth.js file to create the auth routes.

const router = require("express").Router();
const { createUser, loginUser } = require("../Controller/AuthController")

//REGISTER
router.post("/register", createUser);

//LOGIN
router.post("/login", loginUser);

module.exports = router;
Enter fullscreen mode Exit fullscreen mode

To create the post route, paste the code below into the post.js file.

const router = require("express").Router();
const { createPost } = require("../Controller/PostController")

//REGISTER
router.post("/create", createPost);

module.exports = router;
Enter fullscreen mode Exit fullscreen mode

Finally, let us import our routes into the index.js file.

Import the routes as shown below.

const AuthRoute = require("./Routes/auth");
const PostRoute = require("./Routes/post");
Enter fullscreen mode Exit fullscreen mode

Then, use the routes below by pasting the snippet above the app.listen function.

app.use("/api/auth", AuthRoute);
app.use("/api/post", PostRoute)
Enter fullscreen mode Exit fullscreen mode

We have successfully created three typesafe POST routes, “api/auth/register‘, “api/auth/login” and “api/post/create”.

Now, let us test our app.

Test The Application

We will be using the VS Code extension ThunderClient to test our APIs. First, restart the server by killing the terminal and running the command.

node index
Enter fullscreen mode Exit fullscreen mode

Let us begin by testing the “api/auth/register” route.

Image showing the result of testing the register route

In the above image, we tested the register route with the expected data types and got a response status of 200.
Now, let us alter one of the datatypes.

Image showing the result of calling the register route wrongly

Let's look at what we have in the console to get a proper view of the clear error message.

Image showing console message of testing register route wrongly

This shows us the path where the invalid datatype was passed. The errorMessage returns an instance of the StructError class.

Let us test the login route.

Image showing the result of testing login route right

If we pass the expected data type, we should get a status code 200.

Let us test this route by passing in fields not declared in the LoginDetails object.

Image showing result of testing login route wrongly

The console has an array of StructError, with each instance of the StructError giving the details of each error.

Image showing the console result of testing the login route wrongly

Finally, let us test the create post route.

Image showing the result of testing the create post route rightly

Notice how we did not get an error for not passing in the category field. This is because of the optional method we passed in the field.

Let's try adding a category that is not part of the categories declared in the category enum.

Image showing the result of testing create post route wrongly

In the console, we have the error message as shown below.

Image showing console result of testing the create post route wrongly

Wrapping Up

So far, in this tutorial, we have learned how to implement superstruct for basic type-checking in our Express JS server. Type-checking adds an extra layer of security and integrity to our application. Superstruct’s concise error messages also make debugging exercises extremely easy as they identify the precise location of each error with specific or custom error messages.

Moving forward, you can integrate this package into your applications to handle type-checking with just a few lines of code.

I would love to connect with you on Twitter | LinkedIn | Github

Top comments (1)

Collapse
 
markuz899 profile image
Marco

Really very useful
TOP