DEV Community

Anjith Paila
Anjith Paila

Posted on • Originally published at anjith.tech

Github Copilot Best Practices: From Good to Great

Table of Contents


Introduction

This guide assumes you already know the basics: you've installed Copilot, understand tab-to-accept, and you've seen inline completions in action. Now it's time to take one level up. We'll explore techniques that transform Copilot from a simple autocomplete tool into a useful pair programming partner. We will be looking at some code examples to demonstrate the features. Clone the following git repository and open in any copilot supported IDE.

git clone git@github.com:anjithp/ai-code-assistant-demo.git
Enter fullscreen mode Exit fullscreen mode

Part 1: Fundamentals

1.1 Context is everything

The single most important factor in getting quality suggestions from Copilot isn't your prompts: it's your context. Copilot may process all open files in your IDE to understand your codebase patterns.

What this means in practice:

When working on a feature, open all relevant files. For example, If you're building a new React component that fetches tasks from an API, open:

  • The component file you're creating
  • The API service file
  • The TypeScript types file
  • An existing similar component as a reference

What to close:

Close files that aren't relevant to your current task. If you have 20 tabs open from yesterday's debugging session, Copilot's attention is diluted across irrelevant context. Each open file consumes Copilot's limited context window.

Example: Building a task service

Let's say you need to create a new service method in our example project. Here's how context changes the outcome:

Poor context (only taskService.ts open):

// Copilot may suggest generic CRUD code

export const getTaskById = async (id: number) => {
  // Generic suggestion without your patterns
}
Enter fullscreen mode Exit fullscreen mode

Rich context (open taskService.ts, Task.ts model, Category.ts model, and existing similar service):

// Copilot suggests code matching your exact patterns

export const getTaskById = async (id: number) => {
  return await Task.findByPk(id, {
    include: [
      {
        model: Category,
        as: 'category',
        attributes: ['id', 'name', 'color']
      }
    ]
  });
};
Enter fullscreen mode Exit fullscreen mode

The second suggestion matches your project's Sequelize patterns, includes the relationship you always load, and follows your naming conventions: all because Copilot had the right context.

1.2 Prompt Engineering Essentials

After context, the next most important thing to get good results is prompts. The best prompts follow the 3S Principle: Specific, Simple, Short.

Specific: Tell Copilot exactly what you need. Include precise details like desired output format, constraints, or examples. This guides Copilot toward relevant suggestions rather than generic ones.

❌ Bad:

Create a hook

✅ Good:

Custom React hook to fetch and manage tasks with loading and error states.
Returns tasks array, loading boolean, error string, and CRUD methods
Enter fullscreen mode Exit fullscreen mode

Simple: Break complex tasks into smaller steps. Use straightforward language without unnecessary jargon or complexity. Focus on the core intent to make it easy for the AI to understand and respond.

Instead of: "Create a complete authentication system with JWT, refresh tokens, and role-based access control"

Break it down:

Step 1: Create JWT token generation function
Step 2: Create token verification middleware
Step 3: Create refresh token rotation logic
Enter fullscreen mode Exit fullscreen mode

Short: Keep prompts concise to maintain focus: aim for brevity while covering essentials, as longer prompts can dilute the copilot's attention.

❌ Too verbose:

This function should take a task object and update it in the database
but first it needs to validate the task data and make sure all the fields
are correct and if anything is wrong it should throw an error...

✅ Concise:

// Validates and updates task, throws on invalid data
export const updateTask = async (id: number, data: Partial<TaskData>) => {
Enter fullscreen mode Exit fullscreen mode

In summary, keep the prompts as specific to the task in hand, break down when necessary and be concise to the point.

Write detailed comments above function signatures

Comments directly above where you're writing code have the strongest influence on Copilot's suggestions. A well-written comment acts as a specification. It tells Copilot not just what the function does, but how it should behave, what it should return, and any important implementation details.

// Retrieves a single task by ID with associated category
// Returns null if task doesn't exist
// Includes category with id, name, color fields only

export const getTaskById = async (id: number) => {
  return await Task.findByPk(id, {
    include: [
      {
        model: Category,
        as: 'category',
        attributes: ['id', 'name', 'color']
      }
    ]
  });
};
Enter fullscreen mode Exit fullscreen mode

Use inline examples to establish patterns

One of the most effective prompting techniques is showing an example, then letting it generate similar code. This is particularly useful when you're writing repetitive code with slight variations like filter conditions, validation rules, or similar data transformations.

Write the first example manually, add a comment indicating you want more like it, and Copilot will follow the pattern.

// Example: status filter
if (filters.status) {
  where.status = filters.status;
}

// Now generate similar code for priority, categoryId
if (filters.priority) {
  where.priority = filters.priority;  // Copilot follows the pattern
}

if (filters.categoryId) {
  where.categoryId = filters.categoryId;
}
Enter fullscreen mode Exit fullscreen mode

Write test descriptions first in TDD

This could be a good trick if you follow TDD in your development workflow. Test-Driven Development works really well with Copilot. When you write your test first, describing what the function should do and what you expect, Copilot can then generate an implementation that satisfies that specification.

The test acts as both a specification and a validation. Copilot sees what behavior you're testing for and suggests code that produces the expected results.

describe('getTaskStatistics', () => {
  it('should return correct task counts by status', async () => {
    // Arrange: Create 4 tasks (2 pending, 1 in progress, 1 completed)
    // Act: Call getTaskStatistics()
    // Assert: Verify counts match
  });
});

// Now type the implementation. Copilot will suggest code that satisfies this test
Enter fullscreen mode Exit fullscreen mode

1.3 Chat and Inline Completions

Use Inline Completions when:

  • Writing straightforward code with clear patterns
  • Completing functions where the signature gives clear picture of what needs to be done
  • Generating boilerplate code
  • You know exactly what you need

Use Copilot Chat when:

  • You need to understand existing code
  • Refactoring complex logic
  • Debugging errors
  • Exploring multiple approaches
  • Working across multiple files

Use @workspace for codebase-wide questions

The @workspace participant tells Copilot to search your entire codebase to answer a question. This is incredibly useful when you're trying to understand how something works across your project, find where a pattern is used, or locate specific functionality. Instead of using grep or manually searching, ask Copilot to find and explain patterns for you.

Use /explain before /fix when debugging

When you encounter a bug, the temptation is to immediately ask Copilot to fix it. However, using /explain first helps you understand the root cause, which leads to better fixes and helps you learn from the issue.

Powerful Chat Features:

Slash commands are shortcuts to common tasks:

  • /explain – Get a breakdown of complex code
  • /fix – Debug and fix errors
  • /tests – Generate test cases
  • /doc – Create documentation

Chat participants give Copilot specific context:

  • @workspace – Search across your entire workspace
  • #file – Reference specific files: "Update #taskService.ts to use async/await"
  • #codebase – Let Copilot search for the right files automatically

Example chat prompts:

@workspace how do we handle authentication in this codebase?
Show me where JWT tokens are verified.

/explain why is this causing an infinite re-render?
[After understanding the issue]
/fix update the dependency array to prevent re-renders
Enter fullscreen mode Exit fullscreen mode

Part 2: Daily Workflow Optimisation

2.1 Shortcuts & Speed Tricks

Essential shortcuts (VS Code)

  • Tab : Accept suggestion
  • Esc : Dismiss suggestion
  • Ctrl+Enter (Windows/Linux) / Cmd+Enter (Mac) : Open Copilot Chat
  • Alt+] : Next suggestion
  • Alt+[ : Previous suggestion
  • Ctrl+→ : Accept next word of suggestion

Multiple conversation threads

You can have multiple ongoing conversations by clicking the + sign in the chat interface. Use this to:

  • Keep a debugging conversation separate from a feature discussion
  • Maintain context for different tasks
  • Avoid polluting one conversation with unrelated context

Quick accept/reject pattern

When a suggestion is 70-80% correct, it's often faster to accept it and make small edits than to reject it and prompt again. This iterative approach is faster and more productive than waiting for perfect suggestions.

  1. See suggestion → Quickly evaluate (2-3 seconds max)
  2. If 80% correct → Accept with Tab, then edit
  3. If wrong direction → Esc and add clarifying comment
  4. If close but not quite → Alt+] to see alternatives

Build a personal library of effective prompts

As you work with Copilot, you'll discover prompts that consistently produce good results for your codebase. Keep a document with these prompts so you can reuse them. This library becomes more valuable over time as you refine prompts for your specific patterns and needs.

2.2 Custom Instructions

Custom instructions let you teach Copilot your preferences and coding standards. Project-level instructions should be saved in the file .github/copilot-instructions.md. This file acts as a project-wide instruction manual that Copilot reads automatically. It's where you document your tech stack, coding patterns, testing conventions, and any project-specific rules. Think of it as onboarding documentation for Copilot.

Tip: For existing projects, you can put copilot in agent mode, ask it to generate initial instructions file by scanning the repo and make necessary modifications manually.

Example:

# Project Instructions

## Tech Stack
- Backend: Express.js + TypeScript + Sequelize + SQLite
- Frontend: React 19 + TypeScript + Vite

## Code Patterns
- Use functional programming style for services
- All async functions use async/await (never callbacks)
- Services contain business logic, controllers handle HTTP only
- Always include JSDoc comments for exported functions
- Use explicit return types in TypeScript

## Testing
- Tests in `tests/` directory mirror `src/` structure
- Use descriptive test names: "should return 404 when task not found"
- Mock database calls with jest.mock()

## Error Handling
- Controllers throw ApiError for HTTP errors
- Services throw Error with descriptive messages
- Validation errors should specify which field failed
Enter fullscreen mode Exit fullscreen mode

Part 3: Security & Quality

3.1 Do not over rely

The biggest mistake is accepting code you don't understand. Every accepted suggestion should pass this test: "Could I have written this myself given time?" If the answer is no, you're accumulating technical debt or worse critical production incident. In my personal experience, AI assistants have generated buggy and unsafe code several times. Though this is improving you should still be the ultimate judge of the overall quality.

When to write code yourself:

  • Complex business logic unique to your domain
  • Security-critical authentication/authorization
  • Performance-sensitive algorithms
  • Cryptography implementations

3.2 Always review parameterised queries

SQL injection is one of the most common and dangerous security vulnerabilities. While modern ORMs like Sequelize protect you by default, Copilot might occasionally suggest raw queries or string concatenation. Always verify that database queries use parameterized inputs, never string interpolation.

// ❌ Dangerous - SQL injection vulnerability
const tasks = await sequelize.query(
  `SELECT * FROM tasks WHERE status = '${status}'`
);

// ✅ Safe - parameterized query
const tasks = await Task.findAll({
  where: { status }
});
Enter fullscreen mode Exit fullscreen mode

3.3 Verify input validation exists

User input should always be validated before being used in business logic or database operations. Copilot may not always add comprehensive validation, so check that suggested code validates required fields, data types, string lengths, and formats. Missing validation can lead to data corruption, application crashes, or security issues.

export const validateTaskData = (data: Partial<TaskCreationAttributes>) => {
  // Make sure Copilot added proper validation
  if (data.title !== undefined) {
    if (data.title.trim().length < 3) {
      throw new Error('Title must be at least 3 characters long');
    }
  }
  // Check that all required validations are present
};
Enter fullscreen mode Exit fullscreen mode

3.4 Ensure proper error handling

HTTP endpoints should have try-catch blocks(or common error handlers) to handle errors gracefully and return appropriate HTTP status codes. Copilot sometimes generates the happy path without error handling, so always verify that exceptions are caught and handled. Unhandled exceptions crash your server or return 500 errors without useful information.

export const createTask = async (req: Request, res: Response) => {
  try {  // Verify Copilot added error handling
    const task = await taskService.createTask(req.body);
    res.status(201).json({ success: true, data: task });
  } catch (error) {
    // Proper error handling should be here
  }
};
Enter fullscreen mode Exit fullscreen mode

3.5 Check that secrets come from environment variables

Hardcoded secrets in source code are a critical security vulnerability. API keys, database passwords, and JWT secrets must come from environment variables, never be written directly in code. Copilot might suggest hardcoded values for convenience so always replace them with environment variable references.

// ❌ Never accept hardcoded secrets
const secret = 'abc123...';

// ✅ Always use environment variables
const secret = process.env.JWT_SECRET;
if (!secret) {
  throw new Error('JWT_SECRET not configured');
}
Enter fullscreen mode Exit fullscreen mode

3.6 Context Mismanagement

Too many irrelevant files:

Close files from previous tasks. Copilot's context window is limited so better to have only relevant files open.

Not enough context:

Open related files even if you're not editing them. That type definition file, that similar component, they all help to get quality suggestions.

Ignoring project patterns:

If you have a unique architecture or patterns, document them in .github/copilot-instructions.md. Don't expect Copilot to guess.


Summary

Copilot is a powerful tool, but you're still the developer and should have the final say about the code going into production. If you remember the following tips you will go a long way in getting the most value of copilot or any other AI coding assistant.

  1. Manage context – relevant files open, irrelevant files closed
  2. Write clear, specific prompts – following the 3S principle or any other prompting pattern
  3. Use the right tool for the job – chat for exploration, inline for completion
  4. Never blindly accept – every suggestion should be reviewed and understood
  5. Teach patterns – through custom instructions and documentation

Top comments (0)