DEV Community

Cover image for Software Testing for REST APIs: A Practical Guide with TypeScript, Express, and Prisma-MongoDB
Seraphin Brice Kouam
Seraphin Brice Kouam

Posted on

1

Software Testing for REST APIs: A Practical Guide with TypeScript, Express, and Prisma-MongoDB

Introduction: The Crucial Role of Software Testing

In the world of software development, testing is not just an afterthought - it's a critical component that can make or break your project. Did you know that testing typically accounts for about 70% of a project's lifecycle, while development takes up the remaining 30%? This statistic underscores the importance of thorough, well-planned testing strategies.
Software testing ensures that your code does what it's supposed to do, handles errors gracefully, and performs well under various conditions. For REST APIs, which serve as the backbone of many modern applications, robust testing is especially crucial. It helps maintain the reliability and stability of your services, ultimately leading to better user experiences and easier maintenance.
In this article, we'll explore how to implement effective testing for a REST API using TypeScript, Express, and Prisma. We'll focus on integration testing, which verifies that different parts of your application work together as expected.

Setting Up the Project

Before we dive into testing, let's set up our project with the necessary dependencies. Run the following commands in your terminal :

npm init -y
npm install express @prisma/client
npm install -D typescript ts-node @types/express @types/node vitest supertest @types/supertest prisma

Enter fullscreen mode Exit fullscreen mode

Next, let's configure TypeScript. Create a tsconfig.json file in your project root with the following content:

{
  "compilerOptions": {
    "target": "ES2018",
    "module": "CommonJS",
    "lib": ["ES2018", "DOM"],
    "strict": true,
    "esModuleInterop": true,
    "outDir": "dist",
    "rootDir": "src",
    "types": ["vitest/globals", "node"]
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

Enter fullscreen mode Exit fullscreen mode

Setting Up Vitest

To ensure our tests run smoothly, we need to configure Vitest. Create a vitest.config.js file in your project root:

import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    setupFiles: ['dotenv/config'],
    include: ['src/**/*.test.ts'],
    hookTimeout: 60000,
    testTimeout: 30000,
  },
});
Enter fullscreen mode Exit fullscreen mode

This configuration sets up Vitest to work with our TypeScript files and provides adequate timeouts for database operations.

Implementing the API

Let's create a simple user management API. We'll focus on the creation and retrieval of users for this example.

First, let's set up our server in src/server.ts:

import express from 'express'
import { PrismaClient } from '@prisma/client'
import router from './routes/user.routes'

const app = express();
export const prisma = new PrismaClient();

app.use(express.json())
app.use('/users', router)

export default app;
Enter fullscreen mode Exit fullscreen mode

Next, let's implement our user controller in src/controllers/user.controllers.ts

import { Request, Response } from 'express';
import { prisma } from '../server';

export const usersControllers = {
  createUser: async (req: Request, res: Response) => {
    try {
      const { name, email, age } = req.body
      const user = await prisma.user.create({
        data: { name, email, age }
      });
      res.status(201).json(user);
    } catch (error) {
      res.status(400).json({ message: 'Error creating user', error });
    }
  },

  getUsers: async (_req: Request, res: Response) => {
    try {
      const users = await prisma.user.findMany({
        select: { id: true, name: true, email: true }
      });
      res.status(200).json(users);
    } catch (error) {
      res.status(400).json({ message: 'Error fetching users', error });
    }
  },
}

Enter fullscreen mode Exit fullscreen mode

Finally, let's set up our routes in src/routes/user.routes.ts:

import { Router } from 'express';
import { usersControllers } from '../controllers/user.controllers';

const router = Router();

router.post('/', usersControllers.createUser);
router.get('/', usersControllers.getUsers);

export default router;
Enter fullscreen mode Exit fullscreen mode

Writing Integration Tests

Now that we have our basic API set up, let's write some integration tests. Create a file src/userAPI.test.ts:

import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import supertest from 'supertest';
import app from '../server';
import { prisma } from '../server';

const request = supertest(app); //supertest : to make HTTP request to API

describe('User RESTAPI', () => {  //test suite englobing test cases

//1.connection to database before running any test(setup and teardown approach)
  beforeAll(async () => {
    await prisma.$connect();
  }, 60000);
//2.cleaning up the dataase and disconects after doing all tests(set and teardown approach)
  afterAll(async () => {
    await prisma.user.deleteMany();
    await prisma.$disconnect();
  }, 60000);

  //Testcase 1 : Verifies if we can create a user successfully
  it('should create a new user', async () => {
    const res = await request.post('/users').send({ 
      name: 'Briso',
      email: 'example@gmail.com',
      age: 30,
    });

//Tight assertions : checking the status code and response body(postitive Testing)
    expect(res.status).toBe(201);
    expect(res.body).toHaveProperty('id');
    expect(res.body.name).toBe('Briso');
  }, 30000);

  //Testcase 2 : verifies that we can retrieve users
  it('should get all users', async () => {
    const res = await request.get('/users'); //Act
    //Tight assertions : checking the status code and response body(postitive Testing)
    expect(res.status).toBe(200);
    expect(Array.isArray(res.body)).toBeTruthy();
  }, 30000);
Enter fullscreen mode Exit fullscreen mode

Understanding the Tests

  1. Integration tests like these verify that different parts of your application work together correctly. Here's what each test is doing:

  2. User Creation Test: This test sends a POST request to create a new user and checks if the response has the correct status code (201) and contains the expected data. It's testing the flow from the route through the controller to the database and back.

  3. Get All Users Test: This test sends a GET request to retrieve all users and checks if the response has the correct status code (200) and returns an array. It's verifying that our API can correctly retrieve data from the database and send it back to the client.

The expect(Array.isArray(res.body)).toBeTruthy() line is particularly interesting. It's using the toBeTruthy() matcher to check if res.body is indeed an array. This is a way to ensure that our API is returning the correct data structure.

Conclusion

We've just scratched the surface of API testing in this article. There's much more to explore, including testing other CRUD operations, handling edge cases, and implementing more complex validation.
Remember, thorough testing is key to building reliable, maintainable APIs. It might seem like extra work upfront, but it pays off in the long run by catching bugs early and providing confidence in your code.
If you found this article helpful, don't forget to leave a reaction. Also, I'd love to hear your thoughts and experiences in the comments. If you encountered any difficulties or have questions, drop a comment, and I'll be sure to respond.
Want to stay updated on more software techniques and approaches? Follow me here to catch my upcoming articles. Together, let's continue learning and improving our development practices!

Check out the GitHub repo for the full consultation. Feel free to explore, and I'd truly appreciate it if you could leave a star! It would mean a lot :)

GitHub Repo🤝

Happy coding and testing!

Image of Datadog

The Essential Toolkit for Front-end Developers

Take a user-centric approach to front-end monitoring that evolves alongside increasingly complex frameworks and single-page applications.

Get The Kit

Top comments (0)

Image of Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more