Tools: Github Copilot Best Practices: From Good to Great

Tools: Github Copilot Best Practices: From Good to Great

Source: Dev.to

Table of Contents ## Introduction ## Part 1: Fundamentals ## 1.1 Context is everything ## 1.2 Prompt Engineering Essentials ## 1.3 Chat and Inline Completions ## Part 2: Daily Workflow Optimisation ## 2.1 Shortcuts & Speed Tricks ## 2.2 Custom Instructions ## Part 3: Security & Quality ## 3.1 Do not over rely ## 3.2 Always review parameterised queries ## 3.3 Verify input validation exists ## 3.4 Ensure proper error handling ## 3.5 Check that secrets come from environment variables ## 3.6 Context Mismanagement ## Summary 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. 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: 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): Rich context (open taskService.ts, Task.ts model, Category.ts model, and existing similar service): 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. 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. 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" Short: Keep prompts concise to maintain focus: aim for brevity while covering essentials, as longer prompts can dilute the copilot's attention. 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. 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. 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. Use Inline Completions when: Use Copilot Chat when: 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: Chat participants give Copilot specific context: Example chat prompts: Essential shortcuts (VS Code) Multiple conversation threads You can have multiple ongoing conversations by clicking the + sign in the chat interface. Use this to: 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. 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. 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. 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: 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. 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. 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. 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. Too many irrelevant files: Close files from previous tasks. Copilot's context window is limited so better to have only relevant files open. 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. 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. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse COMMAND_BLOCK: git clone [email protected]:anjithp/ai-code-assistant-demo.git Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: git clone [email protected]:anjithp/ai-code-assistant-demo.git COMMAND_BLOCK: git clone [email protected]:anjithp/ai-code-assistant-demo.git COMMAND_BLOCK: // Copilot may suggest generic CRUD code export const getTaskById = async (id: number) => { // Generic suggestion without your patterns } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: // Copilot may suggest generic CRUD code export const getTaskById = async (id: number) => { // Generic suggestion without your patterns } COMMAND_BLOCK: // Copilot may suggest generic CRUD code export const getTaskById = async (id: number) => { // Generic suggestion without your patterns } COMMAND_BLOCK: // 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 COMMAND_BLOCK: // 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'] } ] }); }; COMMAND_BLOCK: // 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'] } ] }); }; CODE_BLOCK: ❌ 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 CODE_BLOCK: ❌ 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 CODE_BLOCK: ❌ 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 CODE_BLOCK: 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 CODE_BLOCK: Step 1: Create JWT token generation function Step 2: Create token verification middleware Step 3: Create refresh token rotation logic CODE_BLOCK: Step 1: Create JWT token generation function Step 2: Create token verification middleware Step 3: Create refresh token rotation logic COMMAND_BLOCK: ❌ 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 COMMAND_BLOCK: ❌ 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>) => { COMMAND_BLOCK: ❌ 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>) => { COMMAND_BLOCK: // 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 COMMAND_BLOCK: // 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'] } ] }); }; COMMAND_BLOCK: // 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'] } ] }); }; CODE_BLOCK: // 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 CODE_BLOCK: // 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; } CODE_BLOCK: // 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; } COMMAND_BLOCK: 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 COMMAND_BLOCK: 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 COMMAND_BLOCK: 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 CODE_BLOCK: @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 CODE_BLOCK: @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 CODE_BLOCK: @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 COMMAND_BLOCK: # 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 COMMAND_BLOCK: # 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 COMMAND_BLOCK: # 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 CODE_BLOCK: // ❌ 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 CODE_BLOCK: // ❌ 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 } }); CODE_BLOCK: // ❌ 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 } }); COMMAND_BLOCK: 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 COMMAND_BLOCK: 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 }; COMMAND_BLOCK: 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 }; COMMAND_BLOCK: 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 COMMAND_BLOCK: 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 } }; COMMAND_BLOCK: 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 } }; CODE_BLOCK: // ❌ 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 CODE_BLOCK: // ❌ 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'); } CODE_BLOCK: // ❌ 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'); } - Introduction - Part 1: Fundamentals 1.1 Context is everything 1.2 Prompt Engineering Essentials 1.3 Chat and Inline Completions - 1.1 Context is everything - 1.2 Prompt Engineering Essentials - 1.3 Chat and Inline Completions - Part 2: Daily Workflow Optimisation 2.1 Shortcuts & Speed Tricks 2.2 Custom Instructions - 2.1 Shortcuts & Speed Tricks - 2.2 Custom Instructions - Part 3: Security & Quality 3.1 Do not over rely 3.2 Always review parameterised queries 3.3 Verify input validation exists 3.4 Ensure proper error handling 3.5 Check that secrets come from environment variables 3.6 Context Mismanagement - 3.1 Do not over rely - 3.2 Always review parameterised queries - 3.3 Verify input validation exists - 3.4 Ensure proper error handling - 3.5 Check that secrets come from environment variables - 3.6 Context Mismanagement - 1.1 Context is everything - 1.2 Prompt Engineering Essentials - 1.3 Chat and Inline Completions - 2.1 Shortcuts & Speed Tricks - 2.2 Custom Instructions - 3.1 Do not over rely - 3.2 Always review parameterised queries - 3.3 Verify input validation exists - 3.4 Ensure proper error handling - 3.5 Check that secrets come from environment variables - 3.6 Context Mismanagement - The component file you're creating - The API service file - The TypeScript types file - An existing similar component as a reference - 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 - You need to understand existing code - Refactoring complex logic - Debugging errors - Exploring multiple approaches - Working across multiple files - /explain – Get a breakdown of complex code - /fix – Debug and fix errors - /tests – Generate test cases - /doc – Create documentation - @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 - 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 - Keep a debugging conversation separate from a feature discussion - Maintain context for different tasks - Avoid polluting one conversation with unrelated context - See suggestion → Quickly evaluate (2-3 seconds max) - If 80% correct → Accept with Tab, then edit - If wrong direction → Esc and add clarifying comment - If close but not quite → Alt+] to see alternatives - Complex business logic unique to your domain - Security-critical authentication/authorization - Performance-sensitive algorithms - Cryptography implementations - Manage context – relevant files open, irrelevant files closed - Write clear, specific prompts – following the 3S principle or any other prompting pattern - Use the right tool for the job – chat for exploration, inline for completion - Never blindly accept – every suggestion should be reviewed and understood - Teach patterns – through custom instructions and documentation