5 Tips for Better AI Coding Output
July 8, 2026
At this point, AI models are extremely capable at writing code. Nevertheless, the output still highly depends on how you use it. The output can vary from "That's a pure AI slop!" to "That looks great, let's ship it!". Let's take a look at 5 tips to help you consistently produce better output.
1. Pack repeatable actions into skills
There are many things we, as developers, regularly do. Adding a new repository, adding a new API, writing a database migration - to name a few examples. Such actions usually follow the same set of steps whenever they need to be done. Instead of explicitly instructing the AI agent every time, we can add a skill. In the skill, we describe how to do something. Then we can either specifically point the AI agent to use it when doing something, but many times they'll just pick it up by themselves. This way, we increase the repeatability as the AI agent always follows the same set of steps.
Example for adding a new repository:
How to Add a New Repository
Phase 1: Domain Model & Database
1. Domain Model (src/{domain}/models/user.py)
from dataclasses import dataclass
import datetime
@dataclass
class User:
id: int | None
email: str
created_at: datetime.datetime
2. Database Model (src/{domain}/db/models_db.py)
from sqlalchemy import Column, BigInteger, String, DateTime, Index
from db.base import Base
class UserRow(Base):
__tablename__ = "users"
id = Column(BigInteger, primary_key=True, autoincrement=True)
email = Column(String, nullable=False, unique=True)
created_at = Column(DateTime, nullable=False)
__table_args__ = (
Index("ix_users_email", "email"),
)
3. Database Migration
alembic revision --autogenerate -m "Add users table"
Use postgresql_concurrently=True for index creation in the migration.
Phase 2: Repository Interface & Implementations
4. Repository (src/{domain}/repositories/user_repository.py)
from abc import ABC, abstractmethod
class IUserRepository(ABC):
@abstractmethod
def create(self, user: User) -> User: ...
@abstractmethod
def get_by_id(self, user_id: int) -> User | None: ...
@abstractmethod
def get_by_email(self, email: str) -> User | None: ...
@abstractmethod
def list_all(self) -> list[User]: ...
class UserRepositoryInMemory(IUserRepository):
def __init__(self) -> None:
self._users: list[User] = []
self._next_id = 1
def create(self, user: User) -> User:
user.id = self._next_id
self._next_id += 1
self._users.append(user)
return user
def get_by_id(self, user_id: int) -> User | None:
return next((u for u in self._users if u.id == user_id), None)
def get_by_email(self, email: str) -> User | None:
return next((u for u in self._users if u.email == email), None)
def list_all(self) -> list[User]:
return self._users
class UserRepositoryPostgres(IUserRepository):
def __init__(self, *, database_session: Session) -> None:
self._session = database_session
def create(self, user: User) -> User:
row = self._row_from_model(user)
self._session.add(row)
self._session.flush()
user.id = row.id
return user
def get_by_id(self, user_id: int) -> User | None:
row = self._session.query(UserRow).filter(UserRow.id == user_id).first()
return self._model_from_row(row) if row else None
def get_by_email(self, email: str) -> User | None:
row = self._session.query(UserRow).filter(UserRow.email == email).first()
return self._model_from_row(row) if row else None
def list_all(self) -> list[User]:
rows = self._session.query(UserRow).order_by(UserRow.created_at.desc()).all()
return [self._model_from_row(r) for r in rows]
@staticmethod
def _model_from_row(row: UserRow) -> User:
return User(id=row.id, email=row.email, created_at=row.created_at)
@staticmethod
def _row_from_model(model: User) -> UserRow:
return UserRow(id=model.id, email=model.email, created_at=model.created_at)
Phase 3: Tests
5. Test Fixture (tests/conftest.py)
@pytest.fixture
def create_user() -> Callable[..., User]:
def _create(**kwargs: Any) -> User:
return User(
id=kwargs.get("id"),
email=kwargs.get("email", "default@example.com"),
created_at=kwargs.get("created_at", datetime.datetime(2025, 9, 6, 10, 0, 0)),
)
return _create
6. Contract Tests (tests/test_io/test_{domain}/test_repositories/test_user_repository.py)
class UserRepositoryContract:
@pytest.fixture
def repository(self) -> IUserRepository:
raise NotImplementedError
def test_get_returns_none_when_empty(self, repository):
assert repository.get_by_id(user_id=999) is None
def test_create_and_retrieve(self, repository, create_user):
user = create_user()
created = repository.create(user)
assert created.id is not None
retrieved = repository.get_by_id(created.id)
assert retrieved is not None
assert retrieved.email == user.email
def test_get_by_email(self, repository, create_user):
user = create_user(email="test@example.com")
repository.create(user)
retrieved = repository.get_by_email("test@example.com")
assert retrieved is not None
assert retrieved.email == "test@example.com"
def test_list_all(self, repository, create_user):
repository.create(create_user(email="a@example.com"))
repository.create(create_user(email="b@example.com"))
assert len(repository.list_all()) == 2
class TestUserRepositoryInMemory(UserRepositoryContract):
@pytest.fixture
def repository(self) -> IUserRepository:
return UserRepositoryInMemory()
class TestUserRepositoryPostgres(UserRepositoryContract):
@pytest.fixture
def repository(self, database_session) -> IUserRepository:
return UserRepositoryPostgres(database_session=database_session)
Phase 4: Quality Checks
make cq # Formatting and linting
make ut # Unit tests
make it # Integration tests
Become a better engineer, one article at a time.
Practices, mindsets, and habits that actually move the needle. Delivered weekly to your inbox.
2. Use a different model for writing vs. reviewing the code
AI code review can be very powerful as described in this post. Anyhow, you'll experience much better results when you use two different models. Similar to human review. The author is less likely to spot the problem due to creator bias. So if you used Opus for coding, use Sonnet for review. For example:
Review this PR for bugs, security issues, and missed edge cases.
Focus on behavior changes, not style.
3. Point to code that does something similar
Often, what you're adding is very similar to something that already exists. For example, your app already supports Google login, and now you want to support GitHub login as well. Let the AI agent know about that when writing a prompt. Prompt example:
Add GitHub login support. Follow the same pattern as the existing Google login:
- OAuth provider: src/auth/providers/google.py
- Auth route: src/auth/routes/google_login.py
- Tests: tests/test_auth/test_google_login.py
Use the same callback flow, token exchange, and user upsert logic.
The GitHub OAuth scopes should be "user:email" and "read:user".
4. Be specific about the behavior
Many times, things are not implemented by AI agents as we never prompt them.
For example, we only request to Implement a job for sending Welcome email with Postmark.
This might already produce a useful result, but being more specific helps to achieve what we want in the first shot.
Ask yourself:
- When should X happen?
- What if there's no user?
- What if there's a retry?
- What if the API is down?
- What if the job is running twice?
Once you know the answers, specify that. For example:
Implement a job to send a welcome email using Postmark.
- It should be triggered at the end of the signup endpoint
- If the user doesn't exist, we should just early exit
- Don't ever retry the job to avoid double sending
5. Shape specification
Sometimes you have only a very high-level idea for a certain feature. In such cases, you should leverage an AI agent to help you shape it. Ask AI agent things like: - Is any information missing? - Do you need more details? - What happens with X if Y? - Do we need to consider race conditions? - Is performance relevant here?
For example:
**You:** I want to add a webhook system so external services can subscribe to events in our app. What do I need to think about before implementing this?
**AI:** A few things to clarify: What events should trigger webhooks - just user-facing actions like "order.created" or also internal ones like "cache.invalidated"? And should subscribers register via API or through a UI?
**You:** Only user-facing events. Registration via API.
**AI:** Got it. What about failed deliveries? If the subscriber's endpoint is down, should we retry? If so, how many times and with what backoff?
**You:** Yes, retry 3 times with exponential backoff. After that, mark it as failed and move on.
**AI:** What about duplicate deliveries? Network issues can cause the same event to be delivered twice. Should we add an idempotency key so subscribers can deduplicate on their end?
**You:** Good point, yes. Include an event ID they can use for deduplication.
**AI:** One more thing - authentication. Should subscribers provide a secret when registering so we can sign payloads with HMAC? That way, they can verify the webhook is genuinely from us.
**You:** Yes, HMAC signing with a shared secret. What about rate limiting? I don't want a single subscriber to overwhelm us with registrations.
**AI:** You could cap webhook registrations per account - say 10 active subscriptions. For delivery, you're already retrying with backoff, which naturally throttles. But you might also want a circuit breaker: if a subscriber's endpoint fails 10 times in a row, auto-disable it and notify them.
**You:** Good, let's do both.
Now you have a clear spec shaped through conversation. You can write a specific prompt based on that.
Conclusion
In this article, we looked at 5 things you can do to improve code generated by AI agents. You might already know some of them, but I hope you learned at least something new from reading this. Try them out and let me know on Twitter or LinkedIn how it goes.
Happy vibing!