TypeScript Tutorial: Building a Simple Web-Based Code Review Request Tool

Code reviews are a cornerstone of modern software development. They help catch bugs, improve code quality, and share knowledge within a team. While many sophisticated tools exist, understanding the fundamentals of building a simple code review request tool can be incredibly valuable. This tutorial will guide you through creating a basic web-based application using TypeScript, providing a solid foundation for understanding the principles behind more complex systems. We’ll focus on the core features: submitting a review request, assigning reviewers, and tracking the review status. By the end, you’ll not only have a working application, but also a deeper understanding of how TypeScript can be used to build robust and maintainable web applications.

Why Build a Code Review Request Tool?

In a team environment, code reviews are essential. They ensure that new code integrates well with existing code, adheres to coding standards, and doesn’t introduce regressions. A dedicated tool streamlines this process, making it easier for developers to submit their code for review, for reviewers to assess the code, and for project managers to monitor the progress of reviews. This tutorial provides a hands-on learning experience, and it’s a great way to understand the underlying principles of code review workflows. It teaches you how to manage data, handle user interactions, and build a simple web interface using TypeScript.

Prerequisites

Before we begin, ensure you have the following:

  • A basic understanding of HTML, CSS, and JavaScript.
  • Node.js and npm (or yarn) installed on your system.
  • A code editor (like Visual Studio Code) for writing your code.

Setting Up Your Project

Let’s start by creating a new project and initializing it with npm. Open your terminal and navigate to the directory where you want to create your project. Then, run the following commands:

mkdir code-review-tool
cd code-review-tool
npm init -y

This will create a new directory for your project and initialize a `package.json` file. Next, let’s install TypeScript and some necessary dependencies:

npm install typescript --save-dev
npm install @types/node --save-dev

These commands install TypeScript and type definitions for Node.js, which will help us write better code. After the installation is complete, create a `tsconfig.json` file in your project root. This file tells the TypeScript compiler how to compile your code. Here’s a basic example:

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"]
}

This configuration specifies that we’re targeting ES5, using CommonJS modules, and outputting the compiled JavaScript to a `dist` directory. The `strict` flag enables strict type checking. Now, let’s create a `src` directory and add our first TypeScript file, `index.ts`.

Creating the Core Data Structures

We’ll start by defining the data structures that represent a code review request. Open `src/index.ts` and add the following code:

// src/index.ts

interface ReviewRequest {
  id: number;
  author: string;
  repository: string;
  branch: string;
  description: string;
  reviewers: string[];
  status: 'open' | 'in_progress' | 'approved' | 'rejected';
}

let reviewRequests: ReviewRequest[] = [];

Here, we define an interface `ReviewRequest` with properties like `id`, `author`, `repository`, `branch`, `description`, `reviewers`, and `status`. The `status` property can have one of the four values: ‘open’, ‘in_progress’, ‘approved’, or ‘rejected’. We also initialize an empty array `reviewRequests` to store our review requests.

Implementing Basic Functions

Next, we’ll implement functions to create, retrieve, and update review requests. Add the following functions to `src/index.ts`:


// Function to generate a unique ID
function generateId(): number {
  return Math.floor(Math.random() * 10000);
}

// Function to create a new review request
function createReviewRequest(
  author: string,
  repository: string,
  branch: string,
  description: string,
  reviewers: string[]
): ReviewRequest {
  const newRequest: ReviewRequest = {
    id: generateId(),
    author,
    repository,
    branch,
    description,
    reviewers,
    status: 'open',
  };
  reviewRequests.push(newRequest);
  return newRequest;
}

// Function to get all review requests
function getReviewRequests(): ReviewRequest[] {
  return reviewRequests;
}

// Function to find a review request by ID
function getReviewRequestById(id: number): ReviewRequest | undefined {
  return reviewRequests.find((request) => request.id === id);
}

// Function to update the status of a review request
function updateReviewRequestStatus(id: number, status: ReviewRequest['status']): ReviewRequest | undefined {
  const requestIndex = reviewRequests.findIndex((request) => request.id === id);
  if (requestIndex !== -1) {
    reviewRequests[requestIndex].status = status;
    return reviewRequests[requestIndex];
  }
  return undefined;
}

These functions provide the basic functionality for managing review requests. The `createReviewRequest` function creates a new review request and adds it to the `reviewRequests` array. The `getReviewRequests` function returns all review requests, and `getReviewRequestById` retrieves a specific request by its ID. The `updateReviewRequestStatus` function allows us to update the status of a review request.

Building a Simple CLI (Command-Line Interface)

To interact with our review request tool, we can create a simple CLI. This will allow us to create, view, and update review requests from the command line. Add the following code to `src/index.ts`:


// src/index.ts (continued)

// Import the 'readline' module for CLI interaction
import * as readline from 'readline';

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

// Function to display the menu
function displayMenu(): void {
  console.log('nCode Review Tool Menu:');
  console.log('1. Create Review Request');
  console.log('2. View All Review Requests');
  console.log('3. View Review Request by ID');
  console.log('4. Update Review Request Status');
  console.log('5. Exit');
}

// Function to handle user input
async function handleInput(): Promise {
  displayMenu();
  rl.question('Enter your choice: ', async (choice) => {
    switch (choice) {
      case '1':
        await createRequestFromCLI();
        break;
      case '2':
        viewAllRequests();
        break;
      case '3':
        await viewRequestByIdFromCLI();
        break;
      case '4':
        await updateRequestStatusFromCLI();
        break;
      case '5':
        rl.close();
        return;
      default:
        console.log('Invalid choice. Please try again.');
    }
    handleInput(); // Re-display menu after action
  });
}

// Functions to gather input for creating a review request
async function createRequestFromCLI(): Promise {
  const author = await question('Enter author: ');
  const repository = await question('Enter repository: ');
  const branch = await question('Enter branch: ');
  const description = await question('Enter description: ');
  const reviewersInput = await question('Enter reviewers (comma-separated): ');
  const reviewers = reviewersInput.split(',').map((reviewer) => reviewer.trim());

  createReviewRequest(author, repository, branch, description, reviewers);
  console.log('Review request created.');
}

// Function to view all review requests
function viewAllRequests(): void {
  const requests = getReviewRequests();
  console.log('nReview Requests:');
  requests.forEach((request) => {
    console.log(`ID: ${request.id}, Author: ${request.author}, Status: ${request.status}`);
  });
}

// Function to view a review request by ID
async function viewRequestByIdFromCLI(): Promise {
  const idInput = await question('Enter request ID: ');
  const id = parseInt(idInput, 10);
  const request = getReviewRequestById(id);
  if (request) {
    console.log('nReview Request Details:');
    console.log(request);
  } else {
    console.log('Review request not found.');
  }
}

// Function to update the status of a review request
async function updateRequestStatusFromCLI(): Promise {
  const idInput = await question('Enter request ID: ');
  const id = parseInt(idInput, 10);
  const status = await question('Enter new status (open, in_progress, approved, rejected): ');
  const updatedRequest = updateReviewRequestStatus(id, status as ReviewRequest['status']);
  if (updatedRequest) {
    console.log('Review request updated.');
  } else {
    console.log('Review request not found.');
  }
}

// Helper function for asking questions
async function question(query: string): Promise {
  return new Promise((resolve) => {
    rl.question(query, resolve);
  });
}

// Start the CLI
handleInput();

This code uses the `readline` module to create a simple CLI. It presents a menu with options to create, view, and update review requests. The `handleInput` function handles user input and calls the appropriate functions based on the user’s choice. The `question` function is a helper function that simplifies asking questions to the user.

Compiling and Running Your Code

Now that we’ve written the code, let’s compile it and run it. In your terminal, run the following commands:

tsc
node dist/index.js

The `tsc` command compiles your TypeScript code into JavaScript, and the `node dist/index.js` command executes the compiled JavaScript. You should now see the menu of your code review tool in the terminal. You can test the different options by entering the corresponding numbers and providing the required information.

Adding Input Validation

Our current implementation lacks input validation, which can lead to unexpected behavior. Let’s add some basic input validation to improve the robustness of our tool. We’ll add validation to the `createRequestFromCLI` and `updateRequestStatusFromCLI` functions. Modify these functions in `src/index.ts` as follows:


// src/index.ts (continued)

async function createRequestFromCLI(): Promise {
  const author = await question('Enter author: ');
  const repository = await question('Enter repository: ');
  const branch = await question('Enter branch: ');
  const description = await question('Enter description: ');
  const reviewersInput = await question('Enter reviewers (comma-separated): ');
  const reviewers = reviewersInput.split(',').map((reviewer) => reviewer.trim());

  // Input validation
  if (!author || !repository || !branch || !description || reviewers.length === 0) {
    console.log('Error: All fields are required.');
    return;
  }

  createReviewRequest(author, repository, branch, description, reviewers);
  console.log('Review request created.');
}

async function updateRequestStatusFromCLI(): Promise {
  const idInput = await question('Enter request ID: ');
  const id = parseInt(idInput, 10);
  const status = await question('Enter new status (open, in_progress, approved, rejected): ');

  // Input validation
  if (isNaN(id)) {
    console.log('Error: Invalid ID. Please enter a number.');
    return;
  }

  if (!['open', 'in_progress', 'approved', 'rejected'].includes(status)) {
    console.log('Error: Invalid status. Please enter a valid status.');
    return;
  }

  const updatedRequest = updateReviewRequestStatus(id, status as ReviewRequest['status']);
  if (updatedRequest) {
    console.log('Review request updated.');
  } else {
    console.log('Review request not found.');
  }
}

We’ve added checks to ensure that all required fields are filled when creating a review request, and that the entered ID is a number and the status is a valid option when updating a request. This simple validation improves the user experience and prevents common errors.

Error Handling

While we’ve added some input validation, we can improve our error handling further. Consider scenarios where the user enters an invalid ID or an unexpected value. Let’s enhance the `getReviewRequestById` function to handle the case where a review request is not found. Modify the function in `src/index.ts` as follows:


// src/index.ts (continued)

// Function to find a review request by ID
function getReviewRequestById(id: number): ReviewRequest | undefined {
  const request = reviewRequests.find((request) => request.id === id);
  if (!request) {
    console.log('Error: Review request not found.');
  }
  return request;
}

Now, if a review request with the given ID is not found, an error message is displayed to the user. This makes the application more user-friendly and helps in debugging.

Adding Unit Tests

Writing unit tests is a crucial part of software development. It helps ensure that your code works as expected and makes it easier to refactor and maintain your code. Let’s add some basic unit tests for our functions. First, install the Jest testing framework:

npm install jest --save-dev

Next, create a new file named `src/index.test.ts` and add the following code:


// src/index.test.ts
import { createReviewRequest, getReviewRequests, getReviewRequestById, updateReviewRequestStatus } from './index';

describe('Review Request Functions', () => {
  beforeEach(() => {
    // Clear the reviewRequests array before each test
    // @ts-ignore
    reviewRequests = [];
  });

  it('should create a new review request', () => {
    const request = createReviewRequest('John Doe', 'my-repo', 'main', 'Fix bug', ['Jane Doe']);
    expect(request.author).toBe('John Doe');
    expect(getReviewRequests().length).toBe(1);
  });

  it('should get all review requests', () => {
    createReviewRequest('John Doe', 'my-repo', 'main', 'Fix bug', ['Jane Doe']);
    createReviewRequest('Jane Doe', 'another-repo', 'develop', 'Add feature', ['John Doe']);
    const requests = getReviewRequests();
    expect(requests.length).toBe(2);
  });

  it('should get a review request by ID', () => {
    const request1 = createReviewRequest('John Doe', 'my-repo', 'main', 'Fix bug', ['Jane Doe']);
    const request2 = getReviewRequestById(request1.id);
    expect(request2).toBe(request1);
  });

  it('should update the status of a review request', () => {
    const request = createReviewRequest('John Doe', 'my-repo', 'main', 'Fix bug', ['Jane Doe']);
    const updatedRequest = updateReviewRequestStatus(request.id, 'approved');
    expect(updatedRequest?.status).toBe('approved');
  });

  it('should return undefined if review request not found when updating status', () => {
    const updatedRequest = updateReviewRequestStatus(9999, 'approved');
    expect(updatedRequest).toBeUndefined();
  });
});

This code defines a series of tests for our functions. The `beforeEach` block ensures that the `reviewRequests` array is cleared before each test, preventing interference between tests. The tests use `expect` to check if the functions behave as expected.

To run the tests, add the following script to your `package.json` file:

{
  "scripts": {
    "test": "jest"
  }
}

Now, run the tests by executing `npm test` in your terminal. Jest will run the tests and report the results. This is a very basic example; you can expand the tests to cover more scenarios and edge cases.

Styling and Enhancements (Optional)

While our CLI-based tool is functional, it lacks a visual interface. To make it more user-friendly, you could consider building a web interface using a framework like React, Angular, or Vue.js. This would allow you to:

  • Display the review requests in a more readable format.
  • Provide a graphical interface for creating and updating requests.
  • Add features like filtering and sorting.

You could also enhance the CLI by adding features such as:

  • Support for different repositories (e.g., GitHub, GitLab).
  • Notifications for new review requests.
  • Integration with a database to persist data.

Key Takeaways

  • TypeScript enhances code quality and maintainability.
  • Interfaces define data structures, improving code readability.
  • Functions encapsulate logic, promoting code reuse.
  • CLI tools provide a simple way to interact with your application.
  • Input validation prevents errors and improves user experience.
  • Unit tests ensure code correctness.

Common Mistakes and How to Fix Them

Here are some common mistakes and how to avoid them:

  • Not using TypeScript: TypeScript helps prevent many common errors by providing static typing. Always use TypeScript for new projects.
  • Ignoring Input Validation: Always validate user input to prevent unexpected behavior.
  • Lack of Error Handling: Implement proper error handling to make your application more robust.
  • Skipping Unit Tests: Write unit tests to ensure that your code works as expected and to make it easier to refactor and maintain your code.
  • Poor Code Structure: Organize your code into logical functions and modules.

FAQ

  1. How can I deploy this tool? You can deploy the CLI version by packaging it into an executable using tools like `pkg` or by running it on a server with Node.js. For a web interface, deploy the front-end (HTML, CSS, JavaScript) to a web server and the back-end (if any) to a Node.js server or a serverless function.
  2. Can I use a database? Yes, you can integrate a database (e.g., MongoDB, PostgreSQL) to persist the review request data. This is essential for any production-ready application.
  3. How can I add authentication? You can add authentication by implementing user accounts and using a library like `bcrypt` to hash passwords.
  4. What are some alternatives to the CLI? You can create a web interface using frameworks like React, Angular, or Vue.js. You can also build a desktop application using Electron.

Building a simple code review request tool with TypeScript is a great way to learn about web application development. While this tutorial covers the basics, the concepts can be applied to more complex projects. As you become more familiar with TypeScript and web development, you can explore adding advanced features, integrating with databases, and building more sophisticated user interfaces. The journey of software development is a continuous process of learning and improvement, and this tutorial provides a solid starting point for your exploration.