DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • AI-Powered Flashcard Application With Next.js, Clerk, Firebase, Material UI, and LLaMA 3.1
  • Instant APIs With Copilot and API Logic Server
  • Self-Hosted Inference Doesn’t Have to Be a Nightmare: How to Use GPUStack
  • The Hidden Risk of SaaS-Based AI: You’re Training Models You Don’t Control

Trending

  • Spring AI Advisors: Chat Memory, Token Tracking, and Message Logging
  • How to Parse Large XML Files in PHP Without Running Out of Memory
  • How to Build an Agentic AI SRE Co-Pilot for Incident Response
  • Amazon Quick: AWS's Agentic Workspace, Explained for Engineers
  1. DZone
  2. Coding
  3. Tools
  4. Building a VS Code-Like Online IDE With Next.js 15, TypeScript, Tailwind CSS, and Goose AI

Building a VS Code-Like Online IDE With Next.js 15, TypeScript, Tailwind CSS, and Goose AI

Build an online IDE with Monaco Editor, Next.js 15, TypeScript, Tailwind CSS, and Goose AI, offering AI-powered code suggestions triggered by comments.

By 
Guhaprasaanth Nandagopal user avatar
Guhaprasaanth Nandagopal
·
Jul. 31, 25 · Tutorial
Likes (2)
Comment
Save
Tweet
Share
3.0K Views

Join the DZone community and get the full member experience.

Join For Free

In this tutorial, we'll build an online IDE inspired by Visual Studio Code using modern web technologies: Next.js 15, TypeScript, Tailwind CSS, and Goose AI's API. This IDE will provide real-time code suggestions based on what you type or any inline comment prompts you to write.

By the end of this guide, you'll have an interactive coding environment featuring:

  • A code editor powered by Monaco Editor (the same editor used in VS Code)
  • Real-time code suggestions as you type or comment (leveraging Goose AI's API)
  • A responsive, modern UI styled with Tailwind CSS

Project Setup

First, let's create a new Next.js 15 project using TypeScript. Open your terminal and run:

TypeScript
 
npx create-next-app@latest online-ide --typescript
cd online-ide


Next, install the dependencies we'll need. We will use:

  • @monaco-editor/react for the code editor
  • Axios for API requests
  • lodash.debounce for debouncing API calls

Run the following command:

Shell
 
npm install @monaco-editor/react axios lodash.debounce


Finally, install Tailwind CSS:

CSS
 
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p


Then, configure your tailwind.config.js by setting the content paths:

JavaScript
 
// tailwind.config.js
module.exports = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx}', 
    './components/**/*.{js,ts,jsx,tsx}'
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}


And add the Tailwind directives to your global CSS file (styles/globals.css):

CSS
 
@tailwind base;
@tailwind components;
@tailwind utilities;


A Closer Look at Next.js and TypeScript Integration

Next.js and TypeScript form a powerful duo for building robust, maintainable web applications. This guide explores their synergy, focusing on server/client rendering, large-scale IDE benefits, and practical type patterns with annotated code samples.

1. How Next.js Simplifies Server/Client Rendering With TypeScript

Next.js provides built-in TypeScript support, enabling type-safe rendering strategies:

A. Static Site Generation (SSG) With getStaticProps

TypeScript
 
// pages/blog/[slug].tsx  
import { GetStaticProps, InferGetStaticPropsType } from 'next';  

// 1. Define type for blog post data  
interface BlogPost {  
  slug: string;  
  title: string;  
  content: string;  
}  

// 2. Type the props using InferGetStaticPropsType  
export default function BlogPage({  
  post  
}: InferGetStaticPropsType<typeof getStaticProps>) {  
  return (  
    <article>  
      <h1>{post.title}</h1>  
      <p>{post.content}</p>  
    </article>  
  );  
}  

// 3. Type-check static props  
export const getStaticProps: GetStaticProps<{ post: BlogPost }> = async ({ params }) => {  
  const res = await fetch(`https://api.example.com/posts/${params?.slug}`);  
  const post: BlogPost = await res.json();  

  // 4. Return typed props (validated at build time)  
  return { props: { post } };  
};


Key benefits:

  • Type inference for props via InferGetStaticPropsType
  • Compile-time validation of API response shapes

B. Server-Side Rendering (SSR) With getServerSideProps

TypeScript
 
// pages/user/[id].tsx  
import { GetServerSideProps } from 'next';  

interface UserProfile {  
  id: string;  
  name: string;  
  email: string;  
}  

export const getServerSideProps: GetServerSideProps<{ user: UserProfile }> = async (context) => {  
  // Type-safe access to route parameters  
  const { id } = context.params as { id: string };
  
  const res = await fetch(`https://api.example.com/users/${id}`);  
  const user: UserProfile = await res.json();  

  return { props: { user } };  
};  

// Component receives type-checked user prop  
export default function UserProfile({ user }: { user: UserProfile }) {  
  return (  
    <div>  
      <h2>{user.name}</h2>  
      <p>{user.email}</p>  
    </div>  
  );  
}


2. TypeScript Benefits in Large-Scale IDE Projects

A. Enhanced Developer Experience

TypeScript
 
// utils/api.ts  
interface ApiResponse<T> {  
  data: T;  
  error?: string;  
}  

// Generic type for API calls  
export async function fetchData<T>(url: string): Promise<ApiResponse<T>> {  
  try {  
    const res = await fetch(url);  
    const data: T = await res.json();  
    return { data };  
  } catch (error) {  
    return { data: null as T, error: error.message };  
  }  
}  

// Usage in component (VS Code shows type hints)  
const { data, error } = await fetchData<UserProfile>('/api/users/123');  
// data is automatically inferred as UserProfile | null


IDE advantages:

  • Auto-completion for API responses
  • Immediate feedback on type mismatches

B. Component Contracts With Props Interfaces

TypeScript
 
// components/Button.tsx  
interface ButtonProps {  
  children: React.ReactNode;  
  variant?: 'primary' | 'secondary';  
  onClick: () => void;  
}  

export const Button = ({ children, variant = 'primary', onClick }: ButtonProps) => {  
  return (  
    <button  
      className={`btn-${variant}`}  
      onClick={onClick}  
    >  
      {children}  
    </button>  
  );  
};  

// Type error if used incorrectly:  
<Button variant="tertiary">Click</Button> // 'tertiary' is not assignable


3. Advanced Type Patterns for Next.js

A. Dynamic Route Params With Type Guards

TypeScript
 
// pages/products/[category].tsx  
import { useRouter } from 'next/router';  

type ValidCategory = 'electronics' | 'books' | 'clothing';  

const ProductCategoryPage = () => {  
  const router = useRouter();  
  const { category } = router.query;  

  // Type guard to validate category  
  const isValidCategory = (value: any): value is ValidCategory => {  
    return ['electronics', 'books', 'clothing'].includes(value);  
  };  

  if (!isValidCategory(category)) {  
    return <div>Invalid category!</div>;  
  }  

  // category is now narrowed to ValidCategory  
  return <div>Showing {category} products</div>;  
};


B. API Route Typing

TypeScript
 
// pages/api/users/index.ts  
import type { NextApiRequest, NextApiResponse } from 'next';  
interface User {  
  id: string;  
  name: string;  
}  

type ResponseData = {  
  users?: User[];  
  error?: string;  
}; 
 
export default function handler(  
  req: NextApiRequest,  
  res: NextApiResponse<ResponseData>  
) {  
  if (req.method === 'GET') {  
    const users: User[] = [  
      { id: '1', name: 'Alice' },  
      { id: '2', name: 'Bob' }  
    ];  
    res.status(200).json({ users });  
  } else {  
    res.status(405).json({ error: 'Method not allowed' });  
  }  
}


C. App-Wide Type Extensions

TypeScript
 
// types/next.d.ts  
import { NextComponentType } from 'next';  

declare module 'next' {  
  interface CustomPageProps {  
    theme?: 'light' | 'dark';  
  }
  
  type NextPageWithLayout<P = {}, IP = P> = NextComponentType<  
    any,  
    IP,  
    P & CustomPageProps  
  > & {  
    getLayout?: (page: ReactElement) => ReactNode;  
  };  
}
  
// Usage in _app.tsx  
type AppProps = {  
  Component: NextPageWithLayout;  
  pageProps: CustomPageProps;  
};  

function MyApp({ Component, pageProps }: AppProps) {  
  const getLayout = Component.getLayout || ((page) => page);  
  return getLayout(<Component {...pageProps} />);  
}


Why TypeScript + Next.js Scales

1. Type-Safe Rendering

  • Validate props for SSG/SSR at build time.
  • Prevent runtime errors in dynamic routes.

2. IDE Superpowers

  • Auto-completion for API responses
  • Instant feedback during development

3. Architectural Integrity

  • Enforce component contracts
  • Maintain consistent data shapes across large teams

To get started:

Shell
 
npx create-next-app@latest --typescript


By combining Next.js' rendering optimizations with TypeScript's type system, teams can confidently build maintainable applications, even at the enterprise scale.

Integrating Monaco Editor

We'll use @monaco-editor/react to embed the Monaco Editor in our Next.js application. The editor will be the main workspace in our IDE.

Create or update the main page at pages/index.tsx with the following code:

JavaScript
 
// pages/index.tsx
import { useState, useCallback, useRef } from 'react';
import dynamic from 'next/dynamic';
import axios from 'axios';
import debounce from 'lodash.debounce';

// Dynamically import the Monaco Editor so it only loads on the client side.
const MonacoEditor = dynamic(
  () => import('@monaco-editor/react').then(mod => mod.default),
  { ssr: false }
);

type CursorPosition = {
  column: number;
  lineNumber: number;
};

const Home = () => {
  // State for storing the editor's current code.
  const [code, setCode] = useState<string>(`// Start coding here...
function helloWorld() {
  console.log("Hello, world!");
}

// Write a comment below to get a suggestion
//`);
  // State for storing the suggestion fetched from Goose AI.
  const [suggestion, setSuggestion] = useState<string>('');
  // State for handling the loading indicator.
  const [loading, setLoading] = useState<boolean>(false);
  // State for handling errors.
  const [error, setError] = useState<string>('');

  // Ref to store the Monaco Editor instance for accessing methods like getPosition.
  const editorRef = useRef<any>(null);

  /**
   * Extracts a prompt from the last line if it starts with `//`.
   *
   * @param codeText - The complete text from the editor.
   * @returns The trimmed comment text or null if not found.
   */
  const extractCommentPrompt = (codeText: string): string | null => {
    const lines = codeText.split('\n');
    const lastLine = lines[lines.length - 1].trim();
    if (lastLine.startsWith('//')) {
      // Remove the comment marker and return the text.
      return lastLine.slice(2).trim();
    }
    return null;
  };

  /**
   * Debounced function to call the Goose AI API.
   * This prevents excessive API calls as the user types.
   */
  const debouncedFetchSuggestion = useCallback(
    debounce((prompt: string, currentCode: string, cursorPosition: CursorPosition) => {
      fetchSuggestion(prompt, currentCode, cursorPosition);
    }, 500),
    []
  );

  /**
   * Calls Goose AI's API with the provided prompt, code context, and cursor position.
   *
   * @param prompt - The comment prompt extracted from the code.
   * @param currentCode - The current content of the editor.
   * @param cursorPosition - The current cursor position in the editor.
   */
  const fetchSuggestion = async (
    prompt: string,
    currentCode: string,
    cursorPosition: CursorPosition
  ) => {
    setLoading(true);
    setError('');
    try {
      // Send a POST request to Goose AI's suggestion endpoint.
      const response = await axios.post(
        'https://api.goose.ai/v1/suggestions',
        {
          prompt,
          codeContext: currentCode,
          cursorPosition,
          language: 'javascript'
        },
        {
          headers: {
            'Authorization': `Bearer ${process.env.NEXT_PUBLIC_GOOSE_AI_API_KEY}`,
            'Content-Type': 'application/json'
          }
        }
      );
      // Update the suggestion state with the returned suggestion.
      setSuggestion(response.data.suggestion);
    } catch (err) {
      console.error('Error fetching suggestion:', err);
      setError('Error fetching suggestion. Please try again.');
    } finally {
      setLoading(false);
    }
  };

  /**
   * Handles changes in the editor. Updates the code state,
   * extracts a prompt (if any), and triggers the debounced API call.
   *
   * @param newValue - The new code from the editor.
   */
  const handleEditorChange = (newValue: string) => {
    setCode(newValue);
    const prompt = extractCommentPrompt(newValue);
    if (prompt) {
      // Retrieve the current cursor position from the editor instance.
      const position = editorRef.current?.getPosition();
      if (position) {
        // Trigger the debounced API call.
        debouncedFetchSuggestion(prompt, newValue, position);
      }
    }
  };

  /**
   * Called when the Monaco Editor is mounted.
   * Stores a reference to the editor instance for later use.
   *
   * @param editor - The Monaco Editor instance.
   */
  const editorDidMount = (editor: any) => {
    editorRef.current = editor;
  };

  /**
   * Inserts the fetched suggestion into the editor at the current cursor position.
   */
  const acceptSuggestion = () => {
    if (editorRef.current && suggestion) {
      const position = editorRef.current.getPosition();
      // Create an edit operation for inserting the suggestion.
      const id = { major: 1, minor: 1 }; // Edit identifier.
      const op = {
        identifier: id,
        // Define the insertion range at the current cursor position.
        range: new editorRef.current.constructor.Range(
          position.lineNumber,
          position.column,
          position.lineNumber,
          position.column
        ),
        text: suggestion,
        forceMoveMarkers: true
      };
      // Execute the edit operation in the editor.
      editorRef.current.executeEdits('insert-suggestion', [op]);
      // Optionally clear the suggestion once inserted.
      setSuggestion('');
    }
  };

  return (
    <div className="flex h-screen">
      {/* Main Code Editor Section */}
      <div className="flex-1">
        <MonacoEditor
          height="100%"
          language="javascript"
          theme="vs-dark"
          value={code}
          onChange={handleEditorChange}
          editorDidMount={editorDidMount}
          options={{
            automaticLayout: true,
            fontSize: 14,
          }}
        />
      </div>

      {/* Suggestion Sidebar */}
      <div className="w-80 p-4 bg-gray-800 text-white overflow-y-auto">
        <h3 className="text-lg font-bold mb-2">Suggestions</h3>
        {loading && <p>Loading suggestion...</p>}
        {error && <p className="text-red-500">{error}</p>}
        {suggestion && (
          <div>
            <pre className="whitespace-pre-wrap bg-gray-700 p-2 rounded">
              {suggestion}
            </pre>
            <button
              onClick={acceptSuggestion}
              className="mt-2 bg-blue-500 hover:bg-blue-600 text-white py-1 px-2 rounded"
            >
              Accept Suggestion
            </button>
          </div>
        )}
        {!loading && !suggestion && !error && (
          <p className="text-gray-400">Type a comment for a suggestion.</p>
        )}
      </div>
    </div>
  );
};

export default Home;


Detailed Code Explanation

1. Dynamic Import of Monaco Editor

We use Next.js's dynamic import to load the Monaco Editor only on the client side (since it relies on the browser environment). This avoids server-side rendering issues:

TypeScript
 
const MonacoEditor = dynamic(
  () => import('@monaco-editor/react').then(mod => mod.default),
  { ssr: false }
);


2. State Management and Editor Reference

  • code: Holds the current code in the editor.
  • suggestion: Stores the suggestion fetched from Goose AI.
  • loading and error: Manage the UI's response during API calls.
  • editorRef: A React ref that gives us direct access to the Monaco Editor's API (e.g., getting the cursor position or executing edits).

3. Extracting the Comment Prompt

The extractCommentPrompt function checks the last line of the code. If it starts with//, it removes the marker and returns the comment text as a prompt for the API.

4. Debouncing API Calls

Using lodash.debounce, we delay the API call until 500 milliseconds have passed after the user stops typing. This minimizes unnecessary requests:

TypeScript
 
const debouncedFetchSuggestion = useCallback(
  debounce((prompt: string, currentCode: string, cursorPosition: CursorPosition) => {
    fetchSuggestion(prompt, currentCode, cursorPosition);
  }, 500),
  []
);


Why Debouncing Is Essential in Real-Time Applications

Consider an online IDE where the user types code and the application provides live feedback (such as linting, code suggestions, or formatting). Each keystroke could trigger an API call without debouncing, quickly overwhelming the server and potentially degrading the user experience.

Benefits of debouncing in real-time applications:

  • Reduced server load: Minimizes the number of API requests by consolidating multiple rapid events into one.
  • Improved performance: Decreases the number of unnecessary operations, making the application more responsive.
  • Better user experience: This feature reduces lag and ensures the application responds only after the user pauses, preventing jittery or overwhelming feedback.

5. Fetching Suggestions From Goose AI

The fetchSuggestion function sends a POST request with the extracted prompt, current code context, and cursor position. It uses an environment variable NEXT_PUBLIC_GOOSE_AI_API_KEY for the API key. (Be sure to add this key to your .env.local file!)

6. Editor Event Handlers

  • handleEditorChange: Updates the code state and triggers the debounced API call if a comment prompt is detected.
  • editorDidMount: Saves the editor instance for our reference for later use.
  • acceptSuggestion: Inserts the fetched suggestion at the current cursor position using Monaco Editor's executeEdits API.

7. Tailwind CSS Styling

We use Tailwind CSS classes to style our application. The editor takes up most of the screen, while a fixed sidebar displays suggestions. The sidebar's styling (e.g., bg-gray-800, text-white, w-80) provides a modern, responsive look.

Connecting to Goose AI's API

Before running the app, create a .env.local file at the project root and add your Goose AI API key:

Shell
 
NEXT_PUBLIC_GOOSE_AI_API_KEY=your_actual_api_key_here


Remember to restart your development server after adding the environment variable. Here's a closer look:

1. Understanding Goose AI's API Endpoints and Parameters

Before integrating the API, it's important to understand the available endpoints and what parameters they expect. For this article, let's assume Goose AI provides an endpoint for code suggestions at:

Shell
 
POST https://api.goose.ai/v1/code-suggestions


Endpoint Parameters

The typical parameters for the code suggestions endpoint might include:

  • code: The current code snippet or document content is provided as a string.
  • language: The programming language of the code (e.g., "javascript", "python").
  • cursorPosition: The current cursor position in the code where suggestions should be made.
  • context (optional): Additional context or project-specific data that can improve suggestions.
  • maxSuggestions (optional): Maximum number of suggestions to return.

A sample request payload in JSON could look like:

JSON
 
{
  "code": "function greet() { 
              console.log('Hello, world!'); 
          }",
  "language": "javascript",
  "cursorPosition": 34,
  "maxSuggestions": 3
}


2. Security Considerations

Security is paramount when integrating any third-party API, especially when dealing with API keys that grant access to paid services. Here are a few best practices for protecting your Goose AI API key:

A. Environment Variables

Store your API key in environment variables rather than hardcoding it into your codebase. For example, in Node.js, you can use a .env file and a package  dotenv to load the key:

Shell
 
# .env file
GOOSE_API_KEY=your-very-secure-api-key


Shell
 
// Load environment variables at the top of your entry file
require('dotenv').config();

// Access your API key securely
const GOOSE_API_KEY = process.env.GOOSE_API_KEY;


B. Server-Side Proxy

For client-side applications, never expose your API key in the browser's JavaScript. Instead, create a server-side proxy endpoint that calls the Goose AI API. This keeps your API key hidden from end users.

JavaScript
 
// server.js
require('dotenv').config();
const express = require('express');
const fetch = require('node-fetch'); // npm install node-fetch@2
const bodyParser = require('body-parser');

const app = express();
const PORT = process.env.PORT || 3000;
const GOOSE_API_KEY = process.env.GOOSE_API_KEY;
const GOOSE_API_URL = 'https://api.goose.ai/v1/code-suggestions';

// Use body-parser to parse JSON bodies
app.use(bodyParser.json());

/**
 * Proxy endpoint to fetch code suggestions from Goose AI.
 * Express is used to create a server with a POST endpoint /api/code-suggestions. This endpoint acts as a proxy.
 */
app.post('/api/code-suggestions', async (req, res) => {
  try {
    // Extract parameters from the client request
    const { code, language, cursorPosition, maxSuggestions } = req.body;
    
    // The server reads the incoming request’s JSON body, then forwards it to the Goose AI API with the proper authorization header.
    const response = await fetch(GOOSE_API_URL, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${GOOSE_API_KEY}`
      },
      body: JSON.stringify({
        code,
        language,
        cursorPosition,
        maxSuggestions: maxSuggestions || 3
      })
    });

    // If the Goose AI API returns an error, the proxy relays that error back to the client with appropriate HTTP status codes.
    if (!response.ok) {
      const errorText = await response.text();
      return res.status(response.status).json({ error: errorText });
    }

    // Send the response back to the client
    const data = await response.json();
    res.json(data);
  } catch (error) {
    console.error('Proxy error:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

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


C. Rate Limiting and Monitoring

Implement rate limiting on your proxy to prevent abuse and monitor API usage to detect suspicious activity.

Putting It All Together

With everything in place, start your development server:

Shell
 
npm run dev


Open http://localhost:3000 in your browser. You'll see a split-screen view:

  • Left panel: A Monaco Editor where you can write JavaScript code.
  • Right panel: A suggestion sidebar that fetches and displays code suggestions when you type a comment (e.g., // Create a function to reverse a string).

When you see a suggestion, click the "Accept Suggestion" button to insert the code into the editor at the current cursor position.

What Is Monaco Editor, and Why Choose It?

Monaco Editor is the code editor that powers Visual Studio Code. It's a robust, fully-featured editor built specifically for the web and designed to handle complex editing scenarios. Here's why it stands out and why you might choose it over other popular editors:

Key Features of Monaco Editor

Rich Language Support

Out of the box, Monaco Editor supports syntax highlighting, IntelliSense (code completion), and error checking for many programming languages. This makes it ideal for building a feature-rich IDE.

Powerful API

Monaco's API allows developers to interact with the editor programmatically. You can control cursor movements, insert or modify code, customize themes, and handle events like text changes. This level of control is particularly useful when building advanced features such as real-time code suggestions or custom code formatting.

Performance

Designed for web applications, Monaco Editor is optimized to handle large files and complex codebases efficiently, ensuring a smooth user experience even for demanding projects.

Customizability

You can deeply customize Monaco Editor's appearance and behavior. Whether you want to modify the default theme, adjust the layout, or integrate with external APIs (like Goose AI for code suggestions), Monaco provides the flexibility required for modern IDEs.

Understanding Monaco Editor's Architecture and API

Architecture Overview

At its core, the Monaco Editor is built on a modular design. Here are some key architectural components:

  • Core editor engine: Handles rendering, editing, and basic language features.
  • Language services: Monaco supports multiple languages through language services that provide syntax highlighting, code completions, error checking, and other features.
  • Theming and styling: The editor can be extensively themed using custom color schemes and tokenization rules.
  • Extension points: Developers can hook into various aspects (e.g., IntelliSense, code actions, hover providers) through well-documented APIs.

Monaco is built to run inside a web browser and relies on asynchronous module definition (AMD) loaders for module management. When you set up Monaco, you load the editor code, register languages and services, and then instantiate the editor within a container element.

API Structure

Monaco's API is organized around several namespaces:

  • monaco.editor: Contains methods for creating and configuring the editor.
  • monaco.languages: Provides APIs to register new languages, define custom tokens, and integrate IntelliSense.
  • monaco.Uri: Utility for handling URIs for files and resources.
  • monaco.Theme: For theming and styling configurations.

The following sections will dive deeper into some APIs with practical examples.

Why Choose Monaco Editor Over Other Editors?

1. Proven Track Record

Being the editor behind VS Code, Monaco has been battle-tested as one of the most popular code editors in the world. Its stability and continuous development make it a reliable choice.

2. Deep Integration Possibilities

Monaco's API offers deep integration with the underlying code, allowing you to implement features like inline code suggestions, custom code actions, and advanced code formatting that might be challenging with simpler editors.

3. Extensibility

Whether you're building a simple code playground or a full-fledged IDE, Monaco can be easily extended and integrated with additional libraries and APIs (such as language servers and AI-based code suggestion services).

Detailed Code Sample With Inline Comments

Below is an example of how you might initialize Monaco Editor in a Next.js application. The code sample includes inline comments to explain key parts of the integration:

JavaScript
 
// Import the Monaco Editor component dynamically.
// This is important for Next.js applications to ensure Monaco is loaded only on the client-side.
import dynamic from 'next/dynamic';
// Dynamically import MonacoEditor to avoid SSR issues, as it relies on browser APIs.
const MonacoEditor = dynamic(
  () => import('@monaco-editor/react').then(mod => mod.default),
  { ssr: false } // Disable server-side rendering for this component.
);
import { useState } from 'react';
const CodeEditorComponent = () => {
  // Define a state variable to hold the code content.
  const [code, setCode] = useState<string>(`// Write your code here...\nfunction greet() {\n  console.log("Hello, world!");\n}\n`);
  // Function to handle changes in the editor's content.
  const handleEditorChange = (value: string | undefined) => {
    // Update the code state with the new value.
    setCode(value || '');
  };
  return (
    // The container for the editor.
    <div style={{ height: '500px', border: '1px solid #ccc' }}>
      {/* 
        MonacoEditor component with key props:
        - height: Defines the height of the editor.
        - language: Specifies the programming language (e.g., JavaScript).
        - theme: Sets the color theme (e.g., "vs-dark").
        - value: Binds the editor content to our state.
        - onChange: Event handler for content changes.
      */}
      <MonacoEditor
        height="100%"
        language="javascript"
        theme="vs-dark"
        value={code}
        onChange={handleEditorChange}
        options={{
          automaticLayout: true, // Auto-adjust the layout based on container size.
          fontSize: 14,          // Set a comfortable font size.
        }}
      />
    </div>
  );
};
export default CodeEditorComponent;


Explanation of the Code Sample

  • Dynamic import: The MonacoEditor It is imported dynamically to ensure it only loads on the client side. This avoids server-side rendering issues in a Next.js environment since Monaco relies on browser-specific APIs.
  • State management: The useState hook is used to manage the code content. Any changes in the editor will update the state via the handleEditorChange function.
  • Editor options: We configure Monaco Editor with options like automaticLayout for responsive resizing and fontSize To adjust the text size. These options help tailor the editor's appearance and behavior to the needs of your application.
  • Event handling: The onChange prop is connected to handleEditorChange, allowing you to capture and react to changes as the user types. This is particularly useful when integrating with features like real-time code suggestions.

Monaco Editor's rich feature set, performance, and flexibility make it ideal for building a modern, browser-based IDE. Whether you're looking to implement advanced code editing features or create a lightweight code playground, Monaco offers a robust foundation that can be tailored to your needs. Its seamless integration with modern frameworks like Next.js and deep customization options set it apart from other editors, making it a popular choice among developers worldwide.

By leveraging Monaco Editor in your projects, you're not just getting a code editor  —  you're getting the power and experience behind one of the world's leading development environments. 

Enhancing the Developer Experience in Your Online IDE

Modern developers crave powerful coding tools and a seamless, customizable, and collaborative environment. Beyond basic code editing and suggestion features, enhancing the developer experience can involve adding live collaboration, debugging tools, Git integration, and personalizing the editor's appearance and behavior. In this article, we'll explore how to implement several of these enhancements using Next.js, TypeScript, Tailwind CSS, and Monaco Editor.

1. Additional Features

A. Live Collaboration

Imagine coding in real-time with colleagues from anywhere in the world. With live collaboration, multiple users can edit the same file simultaneously. A common approach is to use WebSockets for real-time communication. Below is a simplified example demonstrating how to integrate a WebSocket-based collaboration layer.

Note: In a production-grade system, you’d want to add more robust conflict resolution, authentication, and data synchronization mechanisms. This is a minimal proof-of-concept.

Example: WebSocket Integration for Live Collaboration

TypeScript
 
// components/LiveCollaboration.tsx
import { useEffect, useRef, useState } from 'react';

const WS_URL = "wss://your-collaboration-server.example.com"; // Replace with your WebSocket server URL

const LiveCollaboration = () => {
  // Local state to keep the editor content.
  const [content, setContent] = useState<string>('// Collaborative code begins here...\n');
  // A reference to the WebSocket instance.
  const wsRef = useRef<WebSocket | null>(null);

  useEffect(() => {
    // Initialize WebSocket connection.
    wsRef.current = new WebSocket(WS_URL);

    // When a message is received, update the editor content.
    wsRef.current.onmessage = (event) => {
      const data = JSON.parse(event.data);
      // Assuming our server sends an object with a `content` property.
      setContent(data.content);
    };

    // Clean up the WebSocket connection on component unmount.
    return () => {
      wsRef.current?.close();
    };
  }, []);

  /**
   * Sends the updated content to the collaboration server.
   */
  const handleContentChange = (newContent: string) => {
    setContent(newContent);
    if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
      wsRef.current.send(JSON.stringify({ content: newContent }));
    }
  };

  return (
    <div className="border p-4">
      <h2 className="font-bold mb-2">Live Collaboration</h2>
      {/* In a full implementation, you would pass handleContentChange to your Monaco Editor */}
      <textarea
        className="w-full h-48 p-2 border rounded"
        value={content}
        onChange={(e) => handleContentChange(e.target.value)}
      />
    </div>
  );
};

export default LiveCollaboration;


In this example:

  • A WebSocket connection is established when the component mounts.
  • Incoming messages update the local state.
  • Any local changes are sent to the server, enabling live collaboration.

B. Debugging Tools Integration

Enhancing the IDE with debugging capabilities can include integrating a simple debug console or connecting with browser debugging tools. For example, provide a panel that logs runtime errors or output messages.

Example: A Basic Debug Console Component

TypeScript
 
// components/DebugConsole.tsx
import { useState } from 'react';

const DebugConsole = () => {
  // State to store debug messages.
  const [logs, setLogs] = useState<string[]>([]);

  // Function to add a log message.
  const addLog = (message: string) => {
    setLogs(prevLogs => [...prevLogs, message]);
  };

  // Example: simulate adding a log message on a button click.
  const simulateError = () => {
    const errorMessage = "Error: Something went wrong at " + new Date().toLocaleTimeString();
    addLog(errorMessage);
  };

  return (
    <div className="border p-4 mt-4">
      <h2 className="font-bold mb-2">Debug Console</h2>
      <button
        onClick={simulateError}
        className="bg-red-500 text-white py-1 px-2 rounded mb-2"
      >
        Simulate Error
      </button>
      <div className="bg-gray-800 text-green-300 p-2 h-32 overflow-y-auto">
        {logs.map((log, index) => (
          <div key={index} className="text-sm">
            {log}
          </div>
        ))}
      </div>
    </div>
  );
};

export default DebugConsole;
This component provides a simple console that displays error messages or debug output. It's a starting point that you can expand with more sophisticated logging and error handling.

C. Git Integration

Seamless Git integration is key for modern development workflows. While a full integration involves interfacing with Git commands and possibly a backend service, here's a simplified version demonstrating invoking Git operations from your IDE using Node.js (via an API route).

Example: Git Commit via API Route (Next.js)
Server-Side API Route:
// pages/api/git-commit.ts
import { exec } from 'child_process';
import type { NextApiRequest, NextApiResponse } from 'next';

export default (req: NextApiRequest, res: NextApiResponse) => {
  if (req.method !== 'POST') {
    return res.status(405).json({ message: 'Method not allowed' });
  }
  
  // Get the commit message from the request body.
  const { commitMessage } = req.body;

  // Execute a Git commit command.
  exec(`git commit -am "${commitMessage}"`, (error, stdout, stderr) => {
    if (error) {
      console.error(`exec error: ${error}`);
      return res.status(500).json({ error: stderr });
    }
    return res.status(200).json({ message: stdout });
  });
};


2. Client-Side Function to Trigger Git Commit

TypeScript
 
// components/GitIntegration.tsx
import { useState } from 'react';
import axios from 'axios';

const GitIntegration = () => {
  const [commitMessage, setCommitMessage] = useState<string>('');
  const [responseMsg, setResponseMsg] = useState<string>('');

  /**
   * Handles the commit action by sending a POST request to the API.
   */
  const handleCommit = async () => {
    try {
      const res = await axios.post('/api/git-commit', { commitMessage });
      setResponseMsg(res.data.message);
    } catch (err: any) {
      setResponseMsg('Git commit failed: ' + err.response.data.error);
    }
  };

  return (
    <div className="border p-4 mt-4">
      <h2 className="font-bold mb-2">Git Integration</h2>
      <input
        type="text"
        placeholder="Enter commit message"
        value={commitMessage}
        onChange={(e) => setCommitMessage(e.target.value)}
        className="border p-2 rounded w-full mb-2"
      />
      <button
        onClick={handleCommit}
        className="bg-green-500 text-white py-1 px-2 rounded"
      >
        Commit
      </button>
      {responseMsg && <p className="mt-2 text-sm">{responseMsg}</p>}
    </div>
  );
};

export default GitIntegration;


This example demonstrates a simple API route to cGitit changes via Git and a corresponding client-side component to interact with it. For a production IDE, consider integrating librarieGitike isomorphic git for richer functionality.

3. Customizations for a Personalized Experience

A. Theme Switching

Allowing users to switch between themes (such as dark and light mode) can enhance readability and comfort. Below is a code sample demonstrating how to switch themes in your IDE using React state and passing the selected theme to Monaco Editor.

Example: Theme Switcher for Monaco Editor

TypeScript
 
// components/ThemeSwitcher.tsx
import { useState } from 'react';
import dynamic from 'next/dynamic';

// Dynamically import Monaco Editor to avoid SSR issues.
const MonacoEditor = dynamic(() => import('@monaco-editor/react').then(mod => mod.default), { ssr: false });

const ThemeSwitcher = () => {
  // State to hold the current theme.
  const [theme, setTheme] = useState<'vs-dark' | 'light'>('vs-dark');
  // State to hold the editor's content.
  const [code, setCode] = useState<string>(`// Toggle theme with the button below\nfunction greet() {\n  console.log("Hello, world!");\n}\n`);

  /**
   * Toggles between 'vs-dark' and 'light' themes.
   */
  const toggleTheme = () => {
    setTheme((prevTheme) => (prevTheme === 'vs-dark' ? 'light' : 'vs-dark'));
  };

  return (
    <div className="flex flex-col h-full">
      <div className="p-2 bg-gray-200 flex justify-between items-center">
        <h2 className="text-lg font-bold">Theme Switcher</h2>
        <button
          onClick={toggleTheme}
          className="bg-blue-500 text-white py-1 px-2 rounded"
        >
          Toggle Theme
        </button>
      </div>
      <div className="flex-1">
        <MonacoEditor
          height="100%"
          language="javascript"
          theme={theme} // Use the theme state
          value={code}
          onChange={(newValue) => setCode(newValue || '')}
          options={{
            automaticLayout: true,
            fontSize: 14,
          }}
        />
      </div>
    </div>
  );
};

export default ThemeSwitcher;


Here, a simple button toggles between dark and light themes. The selected theme is passed to Monaco Editor, which dynamically changes its appearance.

B. Keyboard Shortcuts

Keyboard shortcuts are essential for boosting developer productivity. For example, you can add shortcuts to save files, switch themes, or trigger code suggestions. Below is an example of using a custom React hook to listen for keyboard events.

Example: Keyboard Shortcut for Saving (Ctrl+S)

TypeScript
 
// hooks/useKeyboardShortcut.ts
import { useEffect } from 'react';

/**
 * Custom hook that listens for a specific key combination and triggers a callback.
 * @param targetKey The key to listen for (e.g., 's' for Ctrl+S).
 * @param callback Function to execute when the shortcut is triggered.
 * @param ctrlRequired Whether the Ctrl key must be pressed.
 */
export const useKeyboardShortcut = (
  targetKey: string,
  callback: () => void,
  ctrlRequired = false
) => {
  useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      if (ctrlRequired && !event.ctrlKey) return;
      if (event.key.toLowerCase() === targetKey.toLowerCase()) {
        event.preventDefault();
        callback();
      }
    };

    window.addEventListener('keydown', handleKeyDown);
    return () => {
      window.removeEventListener('keydown', handleKeyDown);
    };
  }, [targetKey, callback, ctrlRequired]);
};


Using the shortcut in a component:

TypeScript
 
// components/KeyboardShortcutsDemo.tsx
import { useState } from 'react';
import { useKeyboardShortcut } from '../hooks/useKeyboardShortcut';

const KeyboardShortcutsDemo = () => {
  // State to track whether a "save" action was triggered.
  const [saveMessage, setSaveMessage] = useState<string>('');

  // Use the custom hook to trigger "save" on Ctrl+S.
  useKeyboardShortcut('s', () => {
    // Simulate a save action.
    setSaveMessage(`File saved at ${new Date().toLocaleTimeString()}`);
  }, true);

  return (
    <div className="p-4 border mt-4">
      <h2 className="font-bold mb-2">Keyboard Shortcuts Demo</h2>
      <p className="text-sm text-gray-600">Try pressing <code>Ctrl+S</code> to simulate a save.</p>
      {saveMessage && <p className="mt-2 text-green-600">{saveMessage}</p>}
    </div>
  );
};

export default KeyboardShortcutsDemo;


This example uses a custom hook, useKeyboardShortcut, to listen for the "Ctrl+S" key combination. When detected, it triggers a save action (in this case, updating a message), demonstrating how to incorporate keyboard shortcuts to streamline your workflow.

C. Layout Adjustments

Dynamic layout adjustments improve the overall usability of the IDE by letting users customize their workspace. For instance, you might allow users to resize panels or reposition UI elements. Below is a simple example using Tailwind CSS and React state to toggle between different layout configurations.

Example: Toggling Editor and Sidebar Layout

TypeScript
 
// components/LayoutToggle.tsx
import { useState } from 'react';

const LayoutToggle = () => {
  // State to control whether the sidebar is shown.
  const [showSidebar, setShowSidebar] = useState<boolean>(true);

  /**
   * Toggles the visibility of the sidebar.
   */
  const toggleSidebar = () => {
    setShowSidebar((prev) => !prev);
  };

  return (
    <div className="flex h-screen">
      {/* Editor area always takes available space */}
      <div className="flex-1 bg-gray-100 p-4">
        <h2 className="text-xl font-bold">Editor</h2>
        <p>This is your main editing area.</p>
      </div>
      {/* Conditionally render the sidebar */}
      {showSidebar && (
        <div className="w-64 bg-gray-800 text-white p-4">
          <h2 className="font-bold mb-2">Sidebar</h2>
          <p>Additional tools or information can be shown here.</p>
        </div>
      )}
      <div className="absolute top-2 right-2">
        <button
          onClick={toggleSidebar}
          className="bg-blue-500 text-white py-1 px-2 rounded"
        >
          Toggle Sidebar
        </button>
      </div>
    </div>
  );
};

export default LayoutToggle;


3. Boosting Productivity and Creativity

Integrating these enhancements into your IDE offers significant benefits:

  • Real-time collaboration: Enables team members to work together seamlessly, reducing communication barriers and speeding up development cycles.
  • Debugging tools: Provides immediate feedback and error tracking, allowing developers to identify and fix issues quickly.
  • Git integration: Streamlines version control, making it easier to track changes, commit code, and collaborate using standard Git workflows  —  all within the IDE.
  • Customizations (Theme, shortcuts, layout): This would allow developers to tailor the environment to their preferences, enhancing comfort, reducing context switching, and increasing productivity.

By offering a personalized and collaborative coding environment, you empower developers to focus on what they do best: writing high-quality code. These enhancements make the IDE more enjoyable to use and foster creativity and innovation in software development.

Conclusion

In this article, we built a VS Code-like online IDE using Next.js 15, TypeScript, and Tailwind CSS. We integrated Monaco Editor to provide a robust coding environment and connected to Goose AI's API for real-time code suggestions. With debounced API calls, context awareness, and responsive design, this project provides a solid foundation for a modern online IDE.

Feel free to extend this project  by supporting additional languages, enhancing the UI, or adding collaboration features!

Happy coding! If you found this article helpful, please share it and leave feedback in the comments!

AI API Next.js Visual Studio Code

Opinions expressed by DZone contributors are their own.

Related

  • AI-Powered Flashcard Application With Next.js, Clerk, Firebase, Material UI, and LLaMA 3.1
  • Instant APIs With Copilot and API Logic Server
  • Self-Hosted Inference Doesn’t Have to Be a Nightmare: How to Use GPUStack
  • The Hidden Risk of SaaS-Based AI: You’re Training Models You Don’t Control

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook