Before/After Customization Examples
The power of GitHub Copilot customization becomes clear when you see concrete examples of how agents, skills, and instructions transform everyday development workflows. This article presents real-world scenarios showing the dramatic difference between default Copilot behavior and customized experiences that align with your team’s standards, tools, and practices.
Note: The following examples illustrate typical before-and-after scenarios. The actual before and after code may vary depending on the model used and any other context present at generation time.
Example 1: API Client Code Generation
Section titled “Example 1: API Client Code Generation”Before: Generic API Code
Section titled “Before: Generic API Code”Without customization, GitHub Copilot generates generic HTTP request code that may not follow your team’s patterns:
async function getUser(userId: string) { // Default Copilot suggestion const response = await fetch(`https://api.example.com/users/${userId}`); const data = await response.json(); return data;}Problems:
- No error handling or retry logic
- Doesn’t use existing HTTP client utilities
- Missing type safety
- No logging or telemetry
- Hardcoded base URL
After: With Custom Instructions
Section titled “After: With Custom Instructions”Create .github/instructions/typescript-api.md:
---description: 'API client patterns for our application'applyTo: '**/*-api.ts, **/services/**/*.ts'---
When generating API client code:- Always use the HttpClient class from `@/lib/http-client`- Include proper TypeScript types for request/response- Add structured error handling with our ApiError class- Include retry logic for transient failures- Use environment-based configuration for base URLs- Add telemetry using our logger utility- Never hardcode API endpoints
Example pattern:```typescriptimport { httpClient } from '@/lib/http-client';import { ApiError } from '@/lib/errors';import { logger } from '@/lib/logger';
async function fetchResource<T>(endpoint: string): Promise<T> { try { const response = await httpClient.get<T>(endpoint); logger.debug('API call successful', { endpoint }); return response.data; } catch (error) { logger.error('API call failed', { endpoint, error }); throw new ApiError('Failed to fetch resource', error); }}```Now Copilot generates code aligned with your team’s patterns:
import { httpClient } from '@/lib/http-client';import { ApiError } from '@/lib/errors';import { logger } from '@/lib/logger';
interface User { id: string; name: string; email: string;}
async function getUser(userId: string): Promise<User> { try { const response = await httpClient.get<User>(`/users/${userId}`); logger.debug('User fetched successfully', { userId }); return response.data; } catch (error) { logger.error('Failed to fetch user', { userId, error }); throw new ApiError('Unable to retrieve user data', error); }}Benefits:
- Automatically uses your team’s HTTP client
- Includes proper error handling and logging
- Type-safe with your interfaces
- Follows team conventions consistently
- No manual corrections needed
Example 2: Test Generation
Section titled “Example 2: Test Generation”Before: Basic Test Structure
Section titled “Before: Basic Test Structure”Default Copilot test suggestions are often generic and miss project-specific patterns:
import { UserService } from './user-service';
describe('UserService', () => { it('should get user by id', async () => { const service = new UserService(); const user = await service.getUserById('123'); expect(user).toBeDefined(); });});Problems:
- No test fixtures or factories
- Missing setup/teardown
- Doesn’t use testing utilities
- No mocking strategy
- Incomplete assertions
After: With Custom Testing Skill
Section titled “After: With Custom Testing Skill”Create a skill folder .github/skills/generate-tests/ with a SKILL.md:
---name: generate-testsdescription: 'Generate comprehensive test suites using our testing patterns, including fixtures, setup/teardown, and thorough assertions'---
# generate-tests
Generate a comprehensive test suite for the selected code following these patterns:
**Setup Requirements**:- Use our test factory functions from `@/test/factories`- Set up database transactions with `setupTestDb()` and `cleanupTestDb()`- Mock external services using our mock utilities from `@/test/mocks`
**Test Structure**:- Group related tests in `describe` blocks- Use descriptive test names that explain behavior: "should [action] when [condition]"- Include setup/teardown in `beforeEach`/`afterEach`- Test happy path, edge cases, and error scenarios
**Assertions**:- Use specific assertions, not just `.toBeDefined()`- Verify all relevant properties of returned objects- Check error messages and types for failure cases- Assert side effects (database changes, API calls, events)
See [references/test-patterns.md](references/test-patterns.md) for standard patterns and [templates/test-template.ts](templates/test-template.ts) for a starter structure.
**Example Pattern**:```typescriptimport { setupTestDb, cleanupTestDb } from '@/test/setup';import { createUser, createPost } from '@/test/factories';import { mockApiClient } from '@/test/mocks';
describe('UserService', () => { beforeEach(async () => { await setupTestDb(); });
afterEach(async () => { await cleanupTestDb(); });
describe('getUserById', () => { it('should return user when found', async () => { const user = await createUser({ name: 'Alice' }); const service = new UserService();
const result = await service.getUserById(user.id);
expect(result).toEqual({ id: user.id, name: 'Alice', email: expect.any(String) }); });
it('should throw NotFoundError when user does not exist', async () => { const service = new UserService();
await expect(service.getUserById('nonexistent')) .rejects.toThrow(NotFoundError); }); });});```
Generate tests following this pattern for the selected code.Now when you select code and use /generate-tests, or when an agent detects a testing need, Copilot produces comprehensive test suites:
import { setupTestDb, cleanupTestDb } from '@/test/setup';import { createUser, createPost } from '@/test/factories';import { mockApiClient } from '@/test/mocks';import { UserService } from './user-service';import { NotFoundError } from '@/lib/errors';
describe('UserService', () => { let service: UserService;
beforeEach(async () => { await setupTestDb(); service = new UserService(); });
afterEach(async () => { await cleanupTestDb(); });
describe('getUserById', () => { it('should return user with all properties when found', async () => { const user = await createUser({ name: 'Alice Johnson', email: 'alice@example.com' });
const result = await service.getUserById(user.id);
expect(result).toEqual({ id: user.id, name: 'Alice Johnson', email: 'alice@example.com', createdAt: expect.any(Date) }); });
it('should throw NotFoundError when user does not exist', async () => { await expect(service.getUserById('nonexistent-id')) .rejects.toThrow(NotFoundError); });
it('should throw NotFoundError with correct message', async () => { await expect(service.getUserById('invalid')) .rejects.toThrow('User not found'); }); });
describe('getUserPosts', () => { it('should return all posts for a user', async () => { const user = await createUser(); const post1 = await createPost({ userId: user.id, title: 'First Post' }); const post2 = await createPost({ userId: user.id, title: 'Second Post' });
const result = await service.getUserPosts(user.id);
expect(result).toHaveLength(2); expect(result[0]).toMatchObject({ title: 'First Post' }); expect(result[1]).toMatchObject({ title: 'Second Post' }); });
it('should return empty array when user has no posts', async () => { const user = await createUser();
const result = await service.getUserPosts(user.id);
expect(result).toEqual([]); }); });});Benefits:
- Richer test coverage including edge cases
- Uses team’s testing utilities and patterns
- Proper setup/teardown
- Comprehensive assertions
- Ready to run without modifications
Example 3: Infrastructure as Code with Terraform
Section titled “Example 3: Infrastructure as Code with Terraform”Before: Basic Terraform Resources
Section titled “Before: Basic Terraform Resources”Default suggestions lack organization, security best practices, and team conventions:
resource "azurerm_resource_group" "rg" { name = "my-rg" location = "eastus"}
resource "azurerm_storage_account" "data" { name = "mystorage" resource_group_name = azurerm_resource_group.rg.name location = azurerm_resource_group.rg.location account_tier = "Standard" account_replication_type = "LRS" # Defaults used; public network access allowed}Problems:
- No consistent naming aligned to Azure conventions
- Missing tags
- Public network access not blocked; no private endpoint or network rules
- No diagnostic settings for auditing
- Hardcoded values without variables
After: With Terraform Custom Agent
Section titled “After: With Terraform Custom Agent”Use a custom agent from the repository agents/terraform-azure-implement.agent.md (click through to see full definition).
Now Copilot has access to a custom agent that enforces your organization’s Terraform best practices for Azure.
# Azure Storage Account for application data with secure networking and diagnostics
locals { storage_account_name = lower(replace("${var.environment}-${var.project_name}-st-app-data", "-", ""))
common_tags = { Environment = var.environment Project = var.project_name ManagedBy = "Terraform" Owner = var.owner_team CostCenter = var.cost_center }}
resource "azurerm_resource_group" "rg" { name = "${var.environment}-${var.project_name}-rg" location = var.location
tags = merge(local.common_tags, { Name = "${var.environment}-${var.project_name}-rg" })}
resource "azurerm_storage_account" "app_data" { name = local.storage_account_name resource_group_name = azurerm_resource_group.rg.name location = azurerm_resource_group.rg.location account_tier = "Standard" account_replication_type = "ZRS" min_tls_version = "TLS1_2" public_network_access_enabled = false
tags = merge(local.common_tags, { Name = local.storage_account_name Description = "Application data storage account" })}
resource "azurerm_log_analytics_workspace" "law" { name = "${var.environment}-${var.project_name}-law" location = azurerm_resource_group.rg.location resource_group_name = azurerm_resource_group.rg.name sku = "PerGB2018" retention_in_days = 30
tags = local.common_tags}
resource "azurerm_monitor_diagnostic_setting" "sa_diagnostics" { name = "${local.storage_account_name}-diag" target_resource_id = azurerm_storage_account.app_data.id log_analytics_workspace_id = azurerm_log_analytics_workspace.law.id
enabled_log { category = "StorageRead" }
enabled_log { category = "StorageWrite" }
enabled_log { category = "StorageDelete" }
metric { category = "AllMetrics" enabled = true }}
output "storage_account_name" { description = "Name of the application data Storage Account" value = azurerm_storage_account.app_data.name}
output "storage_account_id" { description = "ID of the application data Storage Account" value = azurerm_storage_account.app_data.id}Benefits:
- Secure-by-default configuration (encryption at rest, TLS 1.2+)
- Follows Azure naming and tagging conventions
- Public access blocked with diagnostics enabled
- Clean separation of resources and outputs
- Ready to extend with private endpoints and CMK if required
Example 4: Code Review Comments
Section titled “Example 4: Code Review Comments”Before: Manual Review Process
Section titled “Before: Manual Review Process”Developers manually review pull requests and write comments, which can be time-consuming and inconsistent:
Manual Process:
- Read through code changes
- Think about potential issues
- Write detailed feedback
- Format code suggestions
- Ensure constructive tone
Time investment: 20-30 minutes per PR
After: With Code Review Skill
Section titled “After: With Code Review Skill”Create a skill folder skills/review-pr/ with a SKILL.md:
---name: review-prdescription: 'Generate comprehensive code review with actionable feedback, covering correctness, security, performance, and maintainability'---
# review-pr
Analyze the current git diff and provide a structured code review with:
**Structure**:1. **Summary**: Brief overview of changes2. **Positive Feedback**: What's done well3. **Issues Found**: Problems categorized by severity (Critical/Major/Minor)4. **Suggestions**: Specific improvements with code examples5. **Questions**: Clarifications needed
**Review Focus**:- Code correctness and logic errors- Security vulnerabilities- Performance implications- Test coverage- Code style and maintainability- Error handling- Documentation needs
**Tone**:- Be constructive and helpful- Explain the "why" behind suggestions- Offer specific fixes, not just critique- Acknowledge good patterns
**Format**:Use markdown with code blocks for suggestions. For each issue, provide:- Clear description of the problem- Why it matters- Suggested fix with code example
See [references/review-checklist.md](references/review-checklist.md) for the standard review checklist.
Example format:## 🔴 Critical Issues
### Missing Input Validation**Problem**: The `processUser` function doesn't validate the email format.**Impact**: Could lead to invalid data in the database.**Fix**:```typescriptfunction processUser(user: User) { if (!isValidEmail(user.email)) { throw new ValidationError('Invalid email format'); } // ... rest of function}```Using this skill on a PR (via /review-pr or agent invocation) generates structured, actionable feedback:
## SummaryThis PR adds user authentication with JWT tokens. The implementation includes login, token generation, and middleware for protected routes.
## 🎉 Positive Feedback- Good separation of concerns with separate auth service and middleware- Comprehensive error handling in the login flow- Clear function naming that makes the code self-documenting
## 🔴 Critical Issues
### 1. Hardcoded Secret Key**Problem**: The JWT secret is hardcoded in `auth.service.ts`:```typescriptconst secret = 'my-secret-key'; // ❌ Hardcoded```**Impact**: Security vulnerability - secrets should never be in source code.**Fix**:```typescriptconst secret = process.env.JWT_SECRET;if (!secret) { throw new Error('JWT_SECRET environment variable is required');}```
### 2. Missing Password Hashing Salt Rounds**Problem**: Password hashing uses default bcrypt rounds which may be too low.**Impact**: Weak password protection if defaults are insufficient.**Fix**:```typescriptconst SALT_ROUNDS = 12; // Explicit configurationconst hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);```
## ⚠️ Major Issues
### 3. No Token Expiration Validation**Problem**: The auth middleware doesn't check token expiration explicitly.**Impact**: Expired tokens might be accepted if JWT library doesn't verify.**Fix**: Add explicit expiration check in the middleware.
## 💡 Suggestions
### 4. Consider Adding Refresh TokensThe current implementation uses only access tokens. Consider implementing refresh tokens for better security and user experience:- Short-lived access tokens (15 minutes)- Longer-lived refresh tokens (7 days)- Token rotation on refresh
### 5. Add Rate LimitingThe login endpoint should have rate limiting to prevent brute force attacks:```typescriptimport rateLimit from 'express-rate-limit';
const loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 5, // 5 attempts message: 'Too many login attempts, please try again later'});
app.post('/login', loginLimiter, authController.login);```
## ❓ Questions1. What's the intended token expiration time for production?2. Should we implement token revocation for logout functionality?3. Are there plans for multi-factor authentication in the future?Benefits:
- Consistent review quality
- Faster review process (5-10 minutes)
- Structured, actionable feedback
- Includes code examples for fixes
- Identifies security issues systematically
- Maintains constructive tone
Key Takeaways
Section titled “Key Takeaways”These examples demonstrate how customization transforms GitHub Copilot from a general-purpose assistant into a team-specific expert:
- Instructions embed your team’s patterns into every suggestion automatically
- Skills standardize workflows with bundled resources and enable agent discovery
- Agents bring specialized expertise for complex domains
- Combination of all three creates a comprehensive development assistant
The investment in creating customizations pays dividends through:
- Faster development with fewer manual corrections
- Consistent code quality across the team
- Automatic adherence to best practices
- Reduced onboarding time for new team members
- Better security and maintainability