TypeScript Tutorial: Building a Simple Web-Based Code Search Tool

In the vast expanse of the digital world, where code reigns supreme, the ability to swiftly navigate and understand your codebase is paramount. Imagine you’re knee-deep in a project, and you need to find every instance where a specific function is called or a particular variable is used. Manually sifting through files is a time-consuming and often frustrating task. This is where a code search tool becomes your indispensable ally. In this tutorial, we’ll embark on a journey to build a simple, yet effective, web-based code search tool using TypeScript. This tool will empower you to locate code snippets with ease, boosting your productivity and enhancing your understanding of your projects. We’ll break down the concepts into digestible chunks, providing clear explanations, practical examples, and step-by-step instructions to guide you through the process.

Why Build a Code Search Tool?

Before we dive into the technical details, let’s explore why building a code search tool is a worthwhile endeavor:

  • Efficiency: Quickly locate code snippets, saving you valuable time compared to manual searching.
  • Understanding: Gain a deeper understanding of your codebase by easily identifying how different components interact.
  • Refactoring: Simplify the process of refactoring code by easily finding and updating all instances of a function or variable.
  • Debugging: Identify the source of bugs by quickly locating the code that’s causing the issue.
  • Learning: Enhance your TypeScript skills by building a practical and useful application.

Setting Up the Project

Let’s get started by setting up our project. We’ll be using Node.js and npm (or yarn) to manage our dependencies. If you don’t have these installed, you can download them from the official Node.js website. Create a new directory for your project and navigate into it using your terminal:

mkdir code-search-tool
cd code-search-tool

Next, initialize a new npm project:

npm init -y

This command creates a `package.json` file in your project directory. Now, let’s install the necessary dependencies:

npm install typescript ts-node express

Here’s what each dependency is for:

  • typescript: The TypeScript compiler.
  • ts-node: Allows us to run TypeScript files directly from the command line.
  • express: A web application framework for Node.js, we’ll use it to create our server.

Next, we need to create a `tsconfig.json` file to configure the TypeScript compiler. You can generate one using the following command:

npx tsc --init

This command creates a `tsconfig.json` file with default settings. You can customize these settings to fit your project’s needs. For our project, the default settings should work fine.

Project Structure

Let’s define a simple project structure:

code-search-tool/
├── src/
│   ├── index.ts
│   ├── routes.ts
│   └── utils.ts
├── package.json
├── tsconfig.json
└── .gitignore
  • src/index.ts: The main entry point of our application, responsible for setting up the Express server.
  • src/routes.ts: Defines the API routes for our application.
  • src/utils.ts: Contains utility functions, such as the code search logic.

Implementing the Code Search Logic

The core of our application lies in the code search functionality. Let’s create a utility function in `src/utils.ts` to perform the search. This function will take a search term and a directory path as input, and it will return an array of file paths and line numbers where the search term is found.

// src/utils.ts
import * as fs from 'fs';
import * as path from 'path';

interface SearchResult {
  filePath: string;
  lineNumber: number;
  lineContent: string;
}

export async function searchCode(searchTerm: string, directoryPath: string): Promise<SearchResult[]> {
  const results: SearchResult[] = [];

  async function traverseDirectory(currentPath: string) {
    const files = fs.readdirSync(currentPath);

    for (const file of files) {
      const filePath = path.join(currentPath, file);
      const stat = fs.statSync(filePath);

      if (stat.isDirectory()) {
        await traverseDirectory(filePath);
      } else if (/.(ts|js|jsx|tsx)$/.test(filePath)) {
        // Only search in TypeScript, JavaScript, and React files
        const fileContent = fs.readFileSync(filePath, 'utf-8');
        const lines = fileContent.split('n');

        for (let i = 0; i < lines.length; i++) {
          if (lines[i].includes(searchTerm)) {
            results.push({
              filePath: filePath,
              lineNumber: i + 1,
              lineContent: lines[i],
            });
          }
        }
      }
    }
  }

  await traverseDirectory(directoryPath);
  return results;
}

Let’s break down the code:

  • Import Statements: We import the `fs` (file system) and `path` modules to interact with the file system.
  • SearchResult Interface: Defines the structure of our search results, including the file path, line number, and the content of the line where the search term was found.
  • searchCode Function:
    • Takes a `searchTerm` (the string to search for) and a `directoryPath` (the directory to search within) as input.
    • Initializes an empty `results` array to store the search results.
    • traverseDirectory Function: A recursive function that traverses the directory structure.
      • Reads the contents of the current directory using `fs.readdirSync`.
      • Iterates over the files and directories within the current directory.
      • If a file is a directory, the `traverseDirectory` function is called recursively on that directory.
      • If a file is a `.ts`, `.js`, `.jsx`, or `.tsx` file, it reads the file content using `fs.readFileSync`.
      • Splits the file content into lines.
      • Iterates over the lines and checks if each line includes the `searchTerm`.
      • If the search term is found, it adds a `SearchResult` object to the `results` array.
    • Finally, it returns the `results` array.

Creating the Express Server

Now, let’s create an Express server in `src/index.ts` to handle incoming requests.

// src/index.ts
import express from 'express';
import routes from './routes';

const app = express();
const port = process.env.PORT || 3000;

// Middleware to parse JSON bodies
app.use(express.json());

// Use the routes defined in routes.ts
app.use('/', routes);

app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

Here’s a breakdown of the code:

  • Import Statements: We import `express` and our `routes` module.
  • Create Express App: We create an Express application instance.
  • Set the Port: We define the port the server will listen on, using an environment variable or defaulting to 3000.
  • Middleware: We use `express.json()` middleware to parse JSON request bodies.
  • Routes: We use the routes defined in `routes.ts` by calling `app.use(‘/’, routes)`.
  • Start the Server: We start the server and listen on the specified port.

Defining API Routes

Next, let’s define the API routes in `src/routes.ts`. This file will handle the incoming requests and call the `searchCode` function to perform the search.

// src/routes.ts
import express, { Request, Response } from 'express';
import { searchCode } from './utils';
import path from 'path';

const router = express.Router();

router.post('/search', async (req: Request, res: Response) => {
  const { searchTerm, directoryPath } = req.body;

  if (!searchTerm || !directoryPath) {
    return res.status(400).json({ error: 'searchTerm and directoryPath are required' });
  }

  try {
    const results = await searchCode(searchTerm, directoryPath);
    res.json(results);
  } catch (error: any) {
    console.error(error);
    res.status(500).json({ error: error.message || 'Internal server error' });
  }
});

export default router;

Let’s dissect the code:

  • Import Statements: We import `express`, the `searchCode` function, and `path`.
  • Create Router: We create an Express router instance.
  • /search Route:
    • This route handles POST requests to the `/search` endpoint.
    • It extracts the `searchTerm` and `directoryPath` from the request body.
    • Validation: It checks if `searchTerm` and `directoryPath` are provided. If not, it returns a 400 error.
    • Search Execution: It calls the `searchCode` function with the `searchTerm` and `directoryPath`.
    • Response: It sends the search results as a JSON response. If an error occurs during the search, it catches the error, logs it, and sends a 500 error response.
  • Export Router: We export the router to be used in `index.ts`.

Running the Application

Now that we’ve built the core functionality, let’s run our application. Open your terminal and run the following command:

npx ts-node src/index.ts

This command will compile and run your TypeScript code. You should see a message in the console indicating that the server is running on port 3000 (or the port you configured).

Testing the Code Search Tool

To test our code search tool, we’ll use a tool like `curl` or Postman to send a POST request to the `/search` endpoint. Let’s create a simple test file structure to search within. For example, create a directory called `test-code` and add some `.ts` files inside:

test-code/
├── utils.ts
└── index.ts

Inside `test-code/utils.ts`:

// test-code/utils.ts
export function add(a: number, b: number): number {
  return a + b;
}

Inside `test-code/index.ts`:

// test-code/index.ts
import { add } from './utils';

const result = add(5, 3);
console.log(result);

Now, let’s use `curl` to send a POST request to our server:

curl -X POST -H "Content-Type: application/json" -d '{"searchTerm": "add", "directoryPath": "./test-code