In the fast-paced world of software development, code reviews are essential for maintaining code quality, catching bugs early, and fostering collaboration among developers. However, the process can sometimes be clunky, involving email chains, file attachments, and manual tracking of changes. Wouldn’t it be great to have a dedicated, web-based tool that streamlines the code review process, making it more efficient and enjoyable for everyone involved? This tutorial will guide you through building such a tool using TypeScript, a powerful and versatile language that brings static typing to JavaScript.
Why TypeScript?
TypeScript offers several advantages that make it an excellent choice for this project:
- Static Typing: TypeScript’s static typing helps catch errors during development, reducing the likelihood of runtime bugs.
- Improved Code Readability: Type annotations make your code easier to understand and maintain.
- Enhanced Developer Experience: TypeScript provides better autocompletion, refactoring, and other features that improve developer productivity.
- Scalability: TypeScript is well-suited for building large, complex applications.
Project Overview
Our code review tool will allow users to upload code snippets, add comments, and track the review status of each snippet. We’ll use a simple, yet effective, design focusing on core features to keep the tutorial manageable. Here’s a breakdown of the key components:
- Frontend (React with TypeScript): This is the user interface where users will interact with the tool. They’ll upload code snippets, view and add comments, and see the review status.
- Backend (Node.js with Express and TypeScript): This will handle the server-side logic, including storing code snippets, managing comments, and handling user authentication (optional).
- Database (Optional – e.g., MongoDB): We can use a database to persist data. For simplicity, we’ll initially focus on in-memory storage.
Setting Up the Development Environment
Before we dive into the code, let’s set up our development environment. You’ll need:
- Node.js and npm (or yarn): These are essential for managing dependencies and running our application.
- A code editor (VS Code is highly recommended): VS Code offers excellent TypeScript support.
- TypeScript compiler: We’ll install this globally.
First, install TypeScript globally using npm:
npm install -g typescript
Next, create a new project directory and initialize a new npm project:
mkdir code-review-tool
cd code-review-tool
npm init -y
Now, let’s create our TypeScript configuration file, tsconfig.json. This file tells the TypeScript compiler how to compile your code. In your project directory, create tsconfig.json with the following content:
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"]
}
This configuration specifies that we’ll compile to ES5, use CommonJS modules, output compiled files to a dist directory, and enable strict type checking. It also tells the compiler to include all TypeScript files in the src directory.
Building the Backend (Node.js with Express and TypeScript)
Let’s start by building the backend. We’ll use Node.js, Express, and TypeScript. Create a directory named src in your project directory. Inside src, create a file named index.ts. This will be the entry point for our backend application.
First, install the necessary dependencies:
npm install express @types/express typescript ts-node
Here’s a breakdown of these dependencies:
- express: The Express web framework.
- @types/express: TypeScript type definitions for Express.
- typescript: The TypeScript compiler.
- ts-node: A tool that allows us to run TypeScript files directly without compiling them first (useful for development).
Now, let’s write the code for our backend in src/index.ts:
import express, { Request, Response } from 'express';
const app = express();
const port = process.env.PORT || 3000;
// Middleware to parse JSON request bodies
app.use(express.json());
// In-memory storage for code snippets and comments
interface CodeSnippet {
id: string;
code: string;
comments: Comment[];
status: 'open' | 'in progress' | 'resolved';
}
interface Comment {
id: string;
text: string;
author: string;
createdAt: Date;
}
let codeSnippets: CodeSnippet[] = [];
// API endpoints
// Get all code snippets
app.get('/snippets', (req: Request, res: Response) => {
res.json(codeSnippets);
});
// Create a new code snippet
app.post('/snippets', (req: Request, res: Response) => {
const { code } = req.body;
if (!code) {
return res.status(400).json({ error: 'Code is required' });
}
const newSnippet: CodeSnippet = {
id: String(Date.now()),
code,
comments: [],
status: 'open',
};
codeSnippets.push(newSnippet);
res.status(201).json(newSnippet);
});
// Get a specific code snippet by ID
app.get('/snippets/:id', (req: Request, res: Response) => {
const { id } = req.params;
const snippet = codeSnippets.find((s) => s.id === id);
if (!snippet) {
return res.status(404).json({ error: 'Snippet not found' });
}
res.json(snippet);
});
// Add a comment to a code snippet
app.post('/snippets/:id/comments', (req: Request, res: Response) => {
const { id } = req.params;
const { text, author } = req.body;
const snippet = codeSnippets.find((s) => s.id === id);
if (!snippet) {
return res.status(404).json({ error: 'Snippet not found' });
}
if (!text || !author) {
return res.status(400).json({ error: 'Text and author are required' });
}
const newComment: Comment = {
id: String(Date.now()),
text,
author,
createdAt: new Date(),
};
snippet.comments.push(newComment);
res.status(201).json(newComment);
});
// Update the status of a code snippet
app.put('/snippets/:id/status', (req: Request, res: Response) => {
const { id } = req.params;
const { status } = req.body;
const snippet = codeSnippets.find((s) => s.id === id);
if (!snippet) {
return res.status(404).json({ error: 'Snippet not found' });
}
if (!['open', 'in progress', 'resolved'].includes(status)) {
return res.status(400).json({ error: 'Invalid status' });
}
snippet.status = status;
res.json(snippet);
});
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
Let’s break down this code:
- Imports: We import the
expressmodule and theRequestandResponsetypes. - Express App: We create an Express app instance.
- Port: We set the port number.
- Middleware: We use
express.json()to parse JSON request bodies. - Data Structures: We define interfaces for
CodeSnippetandComment. These interfaces define the structure of our data. - In-Memory Storage: We use an array called
codeSnippetsto store our code snippets. - API Endpoints: We define several API endpoints to handle different operations:
GET /snippets: Retrieves all code snippets.POST /snippets: Creates a new code snippet.GET /snippets/:id: Retrieves a specific code snippet by ID.POST /snippets/:id/comments: Adds a comment to a code snippet.PUT /snippets/:id/status: Updates the status of a code snippet.- Server Listener: We start the server and listen on the specified port.
To run the backend, add the following script to your package.json file in the “scripts” section:
"start": "ts-node src/index.ts"
Now, you can run the backend using the command:
npm start
The backend server should start and listen on port 3000 (or the port specified in your environment variables).
Building the Frontend (React with TypeScript)
Now, let’s build the frontend using React and TypeScript. We’ll use Create React App to quickly set up our project. In your project directory, run the following command:
npx create-react-app frontend --template typescript
This command creates a new React app named “frontend” with TypeScript support. After the command completes, navigate to the “frontend” directory:
cd frontend
Now, let’s install some additional dependencies we’ll need:
npm install axios bootstrap react-bootstrap
Here’s a breakdown:
- axios: A library for making HTTP requests to our backend.
- bootstrap: A CSS framework for styling our application.
- react-bootstrap: React components for Bootstrap.
Now, let’s modify the src/App.tsx file. Replace the contents of src/App.tsx with the following code:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import 'bootstrap/dist/css/bootstrap.min.css';
import { Container, Row, Col, Button, Form, Card, Alert } from 'react-bootstrap';
interface CodeSnippet {
id: string;
code: string;
comments: Comment[];
status: 'open' | 'in progress' | 'resolved';
}
interface Comment {
id: string;
text: string;
author: string;
createdAt: string;
}
function App() {
const [codeSnippets, setCodeSnippets] = useState([]);
const [newCode, setNewCode] = useState('');
const [selectedSnippetId, setSelectedSnippetId] = useState(null);
const [newCommentText, setNewCommentText] = useState('');
const [newCommentAuthor, setNewCommentAuthor] = useState('');
const [commentAddedAlert, setCommentAddedAlert] = useState(false);
const [codeAddedAlert, setCodeAddedAlert] = useState(false);
const [statusUpdatedAlert, setStatusUpdatedAlert] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
useEffect(() => {
fetchCodeSnippets();
}, []);
const fetchCodeSnippets = async () => {
try {
const response = await axios.get('http://localhost:3000/snippets');
setCodeSnippets(response.data);
} catch (error: any) {
setErrorMessage(error.message || 'Failed to fetch snippets.');
}
};
const handleAddCode = async () => {
try {
const response = await axios.post('http://localhost:3000/snippets', { code: newCode });
setCodeSnippets([...codeSnippets, response.data]);
setNewCode('');
setCodeAddedAlert(true);
setTimeout(() => setCodeAddedAlert(false), 3000);
} catch (error: any) {
setErrorMessage(error.message || 'Failed to add code.');
}
};
const handleAddComment = async () => {
if (!selectedSnippetId) return;
try {
await axios.post(`http://localhost:3000/snippets/${selectedSnippetId}/comments`, {
text: newCommentText,
author: newCommentAuthor,
});
// Refetch snippets to update the comments
fetchCodeSnippets();
setNewCommentText('');
setNewCommentAuthor('');
setCommentAddedAlert(true);
setTimeout(() => setCommentAddedAlert(false), 3000);
} catch (error: any) {
setErrorMessage(error.message || 'Failed to add comment.');
}
};
const handleUpdateStatus = async (snippetId: string, status: 'open' | 'in progress' | 'resolved') => {
try {
await axios.put(`http://localhost:3000/snippets/${snippetId}/status`, { status });
fetchCodeSnippets();
setStatusUpdatedAlert(true);
setTimeout(() => setStatusUpdatedAlert(false), 3000);
} catch (error: any) {
setErrorMessage(error.message || 'Failed to update status.');
}
};
return (
<h1>Code Review Tool</h1>
{errorMessage && {errorMessage}}
<Col>
<h2>Add Code Snippet</h2>
Code
setNewCode(e.target.value)}
/>
<Button>Add Code</Button>
{codeAddedAlert && Code snippet added!}
</Col>
<Col>
<h2>Code Snippets</h2>
{codeSnippets.map((snippet) => (
Snippet ID: {snippet.id}
<Button> setSelectedSnippetId(snippet.id === selectedSnippetId ? null : snippet.id)}
>
{snippet.id === selectedSnippetId ? 'Hide Details' : 'Show Details'}
</Button>
{snippet.id === selectedSnippetId && (
<b>Status:</b> {snippet.status}
<b>Code:</b>
<pre style="{{"><code>{snippet.code}
Comments
{snippet.comments.map((comment) => (
Author: {comment.author}
{comment.text}
{new Date(comment.createdAt).toLocaleString()}
))}
Add Comment
setNewCommentText(e.target.value)}
/>
setNewCommentAuthor(e.target.value)}
/>
{commentAddedAlert && Comment added!}
{statusUpdatedAlert && Status updated!}
)}
))}
);
}
export default App;
Let’s break down this code:
- Imports: We import React hooks (
useState,useEffect),axiosfor making API requests, Bootstrap CSS, and React Bootstrap components. - Interfaces: We define interfaces for
CodeSnippetandComment, matching the structure of our backend data. - State Variables: We use
useStateto manage the following states: codeSnippets: An array of code snippets fetched from the backend.newCode: The code entered by the user in the “Add Code Snippet” form.selectedSnippetId: The ID of the currently selected code snippet (for showing/hiding details).newCommentText: The text of the new comment.newCommentAuthor: The author of the new comment.commentAddedAlert: A flag to show the “comment added” alert.codeAddedAlert: A flag to show the “code added” alert.statusUpdatedAlert: A flag to show the “status updated” alert.errorMessage: To store and display error messages.- useEffect Hook: The
useEffecthook is used to fetch code snippets from the backend when the component mounts. - fetchCodeSnippets function: Fetches the code snippets from the backend.
- handleAddCode function: Handles adding a new code snippet.
- handleAddComment function: Handles adding a new comment to a snippet.
- handleUpdateStatus function: Handles updating the status of a snippet.
- JSX: The JSX code renders the user interface. It includes:
- A form to add new code snippets.
- A list of code snippets, displaying their ID, status, and comments.
- A form to add comments to a selected snippet.
- Buttons to update the status of each snippet.
To run the frontend, start the development server using:
npm start
This will start the React development server, and you should be able to access the application in your web browser (usually at http://localhost:3000).
Connecting Frontend and Backend
Ensure that your backend server is running. The frontend application, by default, will try to connect to http://localhost:3000. If your backend is running on a different port, you will need to adjust the API calls in the frontend accordingly (e.g., in the fetchCodeSnippets, handleAddCode, handleAddComment, and handleUpdateStatus functions). Also, ensure that your CORS (Cross-Origin Resource Sharing) configuration allows requests from your frontend’s origin to your backend’s origin.
Testing and Debugging
Testing and debugging are critical steps in software development. Here’s how to approach these for our code review tool:
- Frontend Testing:
- Manual Testing: Manually test the frontend by adding code snippets, adding comments, and changing the status of the snippets.
- Browser Developer Tools: Use your browser’s developer tools (e.g., Chrome DevTools) to inspect network requests, check for errors in the console, and debug your JavaScript code.
- Unit Tests (Optional): If you want more robust testing, consider writing unit tests for your React components using a testing framework like Jest and React Testing Library.
- Backend Testing:
- Manual Testing: Use tools like Postman or curl to send requests to your backend API endpoints and verify that they return the expected responses.
- Console Logging: Add console logs in your backend code to trace the execution flow and debug any issues.
- Unit Tests (Optional): Write unit tests for your backend API endpoints and business logic using a testing framework like Jest.
- Debugging Tips:
- Check the Browser Console: The browser console is your best friend for debugging frontend issues. Look for error messages, warnings, and console logs.
- Check the Backend Console: The backend console shows server-side logs and any errors that occur.
- Use a Debugger: Use a debugger in your code editor (e.g., VS Code) to step through your code line by line, inspect variables, and identify the source of bugs.
- Inspect Network Requests: In the browser developer tools, inspect the network requests to see if your frontend is sending the correct requests to the backend and if the backend is returning the expected responses.
Common Mistakes and How to Fix Them
Here are some common mistakes and how to avoid them:
- Incorrect API Endpoint URLs: Double-check that your frontend API calls are using the correct URLs for your backend endpoints. Typos are a common source of errors.
- CORS Issues: Make sure your backend is configured to allow requests from your frontend’s origin (e.g.,
http://localhost:3000). You can configure CORS in your Express app using thecorsmiddleware. - Type Errors: TypeScript helps catch type errors, but you still need to be careful. Make sure you’re using the correct types for your variables and function parameters. Use type annotations to make your code more readable and prevent errors.
- State Management Issues: When working with React, ensure that you’re updating the state correctly using the
useStatehook. Avoid directly modifying the state variables. - Unhandled Errors: Always include error handling in your API calls (using
try...catchblocks) to catch any potential errors and display informative error messages to the user.
Enhancements and Next Steps
This is a basic code review tool. Here are some ideas for future enhancements:
- User Authentication: Implement user authentication to secure the tool and allow users to log in and manage their code snippets.
- Database Integration: Integrate a database (e.g., MongoDB, PostgreSQL) to persist the code snippets, comments, and user data.
- Code Highlighting: Use a code highlighting library (e.g., Prism.js, highlight.js) to display code snippets with syntax highlighting.
- Code Formatting: Integrate a code formatting tool (e.g., Prettier) to automatically format code snippets.
- Notifications: Implement notifications to notify users of new comments or status updates.
- Real-time Updates: Use WebSockets to provide real-time updates (e.g., comments) to all users.
- Version Control Integration: Integrate with a version control system (e.g., Git) to import code snippets from repositories.
- More Sophisticated Statuses: Add more statuses, such as “Needs Review,” “Approved,” and “Changes Requested.”
Summary / Key Takeaways
You’ve successfully built a basic, web-based code review tool using TypeScript, React, and Node.js. This project demonstrates the power of TypeScript for building robust and maintainable applications. You’ve learned how to set up a development environment, structure your code, create API endpoints, handle user input, and manage state in a React application. Remember to test your code thoroughly and consider adding the enhancements we discussed. With the skills you’ve acquired, you’re well-equipped to tackle more complex web development projects. Building this tool helps you understand the benefits of TypeScript, the fundamentals of frontend and backend development, and the importance of code reviews in a collaborative software development environment.
FAQ
Q: Can I use a different database?
A: Yes, you can use any database you prefer. You would need to install the appropriate Node.js package for your chosen database (e.g., mongoose for MongoDB, pg for PostgreSQL) and modify the backend code to interact with the database.
Q: How do I deploy this application?
A: You can deploy your application to various platforms, such as Heroku, Netlify, or AWS. You’ll need to configure your deployment environment to run both the frontend and backend applications. This typically involves setting up environment variables, configuring a database connection, and deploying the code.
Q: How can I improve the user interface?
A: You can enhance the user interface by using a more advanced UI framework (e.g., Material UI, Ant Design), adding more styling, and improving the overall user experience. Consider using a design system to create a consistent and visually appealing UI.
Q: What are some good resources for learning more about TypeScript?
A: The official TypeScript documentation is an excellent resource. You can also find many tutorials, articles, and online courses on platforms like Udemy, Coursera, and freeCodeCamp. Exploring open-source projects using TypeScript is another great way to learn.
By following this tutorial, you’ve gained practical experience in building a web application with TypeScript, reinforcing your understanding of the language, and solidifying your skills in both frontend and backend development. This project serves as a solid foundation for more complex web development projects, and your ability to create this code review tool demonstrates a significant step forward in your journey as a developer.
