<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Lubnaaly on Medium]]></title>
        <description><![CDATA[Stories by Lubnaaly on Medium]]></description>
        <link>https://medium.com/@lubnaaly12?source=rss-df9e89e9c50c------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/0*itsCPJc8To6ggXc2</url>
            <title>Stories by Lubnaaly on Medium</title>
            <link>https://medium.com/@lubnaaly12?source=rss-df9e89e9c50c------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 14 Nov 2026 05:13:35 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@lubnaaly12/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[5 Claude Tricks That Give You an Unfair Advantage (Most People Still Use the Old Way)]]></title>
            <link>https://medium.com/codetodeploy/5-claude-tricks-that-give-you-an-unfair-advantage-most-people-still-use-the-old-way-cdadb1091a25?source=rss-df9e89e9c50c------2</link>
            <guid isPermaLink="false">https://medium.com/p/cdadb1091a25</guid>
            <category><![CDATA[business]]></category>
            <category><![CDATA[technology]]></category>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[math]]></category>
            <dc:creator><![CDATA[Lubnaaly]]></dc:creator>
            <pubDate>Fri, 13 Nov 2026 14:07:57 GMT</pubDate>
            <atom:updated>2026-11-13T14:07:57.122Z</atom:updated>
            <content:encoded><![CDATA[<h4><em>5 practical Claude workflows that changed how I research, code, automate tasks, and get more done without constantly rewriting prompts.</em></h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*bZ_piJU9LpwcQja4" /><figcaption>Photo by <a href="https://unsplash.com/@planetvolumes?utm_source=medium&amp;utm_medium=referral">Planet Volumes</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>Most people use Claude the same way.</p><p>They open a new conversation, type a prompt, get an answer, make a few corrections, and repeat the entire process the next day.</p><p>There’s nothing wrong with that.</p><p>But after spending time experimenting with AI-assisted development and productivity workflows, I’ve realized something: the biggest advantage doesn’t come from writing better prompts. It comes from changing how you work with the tool.</p><p>Claude has several capabilities that can make a surprisingly big difference when you use them properly.</p><p>Some help you avoid repeating instructions. Others let you delegate complicated tasks, reuse entire workflows, or make Claude check its own work.</p><p>The interesting part?</p><p>Many users still treat Claude like a chatbot when they could be using it as a much more capable working environment.</p><p>Here are five tricks worth learning.</p><h3>1. Stop Rewriting Instructions — Create Your Own Claude Skills</h3><p>One of the most frustrating things about working with AI is repetition.</p><p>Imagine you’re a Python developer.</p><p>Every time you ask Claude to write code, you explain that you prefer:</p><ul><li>Type hints.</li><li>PEP 8 formatting.</li><li>Clear function names.</li><li>Modular architecture.</li><li>Proper exception handling.</li><li>Unit tests for important functions.</li></ul><p>You might spend more time explaining your preferences than actually reviewing the code.</p><p>That’s where Claude Skills become interesting.</p><p>A Skill is essentially a reusable set of instructions, resources, and sometimes scripts that teaches Claude how to perform a particular task.</p><p>Instead of explaining your workflow in every conversation, you define it once and reuse it.</p><p>For example, you could create a Python code-review Skill that instructs Claude to:</p><ol><li>Inspect the code before suggesting changes.</li><li>Identify performance bottlenecks.</li><li>Look for unnecessary database queries.</li><li>Check error handling and edge cases.</li><li>Suggest improvements without unnecessarily rewriting the entire application.</li><li>Include tests for important changes.</li></ol><p>Now imagine using the same workflow across multiple projects.</p><p>You don’t need to paste six paragraphs of instructions every time.</p><p><strong>Here’s a prompt to get started:</strong></p><p>“Create a reusable Claude Skill for reviewing production-level Python code. It should check code quality, performance, security, maintainability, error handling, and testing. Include clear instructions and examples. Keep the review practical and prioritize problems by severity.”</p><p>The interesting thing about Skills is that Claude can discover relevant skills and load their detailed instructions when needed, rather than requiring you to paste everything into every conversation.</p><p>That’s a much better way to handle repetitive work.</p><p><strong>When I would use it:</strong> Code reviews, documentation generation, research workflows, report writing, and any task where consistency matters.</p><h3>2. Give Claude a Project Memory Instead of Starting From Zero</h3><p>Here’s a situation most developers have experienced.</p><p>You’re working on a project for several days.</p><p>Claude understands your architecture, database structure, naming conventions, and business logic.</p><p>Then you start another conversation.</p><p>Suddenly, you’re explaining everything again.</p><p>It’s like onboarding a new developer every morning.</p><p>Claude Projects can help solve this problem.</p><p>Projects let you organize related conversations, files, and project-specific instructions in one place.</p><p>For example, suppose you’re building a FastAPI application.</p><p>You could create a Project containing:</p><ul><li>Your project architecture.</li><li>Database schema.</li><li>API documentation.</li><li>Coding conventions.</li><li>Important configuration details.</li><li>Existing implementation decisions.</li></ul><p>You can also add project instructions explaining how Claude should approach development.</p><p>For example:</p><p>“You’re helping me develop a production-ready FastAPI application. Before suggesting architectural changes, inspect the existing project structure and understand how its components interact.</p><p>Use Pydantic for validation, SQLAlchemy for database operations, and follow modular architecture. Avoid unnecessary dependencies and explain potentially breaking changes before recommending them.”</p><p>Now your project has a consistent starting point.</p><p>You can ask Claude to implement an endpoint, investigate a database issue, or review a particular module without repeatedly explaining the entire application.</p><p>One important distinction: project knowledge and conversation history aren’t identical. Put important reusable information into project instructions or uploaded knowledge rather than assuming Claude will remember every decision automatically.</p><p><strong>When I would use it:</strong> Long-running software projects, research, business documentation, and any workflow involving multiple related conversations.</p><h3>3. Ask Claude to Plan Before Writing Code</h3><p>This is one of the simplest changes I’ve made to my AI-assisted development workflow.</p><p>Previously, I would give Claude a task and expect it to start coding immediately.</p><p>Sometimes it worked beautifully.</p><p>Other times, it modified the wrong files, misunderstood the architecture, or implemented something that technically worked but created problems elsewhere.</p><p>The issue wasn’t necessarily the model.</p><p>It was the workflow.</p><p>For complicated development tasks, I now prefer separating planning from implementation.</p><p>Instead of asking:</p><p>“Build an authentication system for my FastAPI application.”</p><p>I use a different approach.</p><p><strong>Step 1: Ask Claude to investigate.</strong></p><p>“Inspect my existing FastAPI project. Identify the current authentication mechanism, database models, API structure, and relevant dependencies. Don’t modify any files yet.”</p><p><strong>Step 2: Ask for an implementation plan.</strong></p><p>“Based on the existing architecture, create a step-by-step implementation plan for JWT authentication. Include the files that need modification, required dependencies, security considerations, and potential edge cases.”</p><p><strong>Step 3: Review the plan.</strong></p><p>This is where you catch architectural mistakes before they become code.</p><p><strong>Step 4: Ask Claude to implement the approved plan.</strong></p><p>“Implement the approved authentication plan. Follow the existing coding conventions and avoid unrelated changes.”</p><p>This approach is particularly useful with Claude Code.</p><p>It allows you to investigate a codebase, understand the proposed changes, and then move into implementation with a much clearer direction.</p><p>There’s another advantage.</p><p>You can use subagents for independent investigation tasks in Claude Code, allowing different pieces of a complicated problem to be explored separately.</p><p>For example:</p><ul><li>One agent investigates database architecture.</li><li>Another reviews authentication security.</li><li>Another examines existing API endpoints.</li></ul><p>You can then combine their findings before implementing anything.</p><p>The goal isn’t to make Claude write more code.</p><p>It’s to make sure it writes the right code.</p><h3>4. Make Claude Check Its Own Work</h3><p>Here’s something I wish more people understood about AI-assisted programming.</p><p>Getting code from an AI model is relatively easy.</p><p>Knowing whether that code actually works is a different problem.</p><p>A function might look perfectly reasonable and still fail when it encounters unexpected input.</p><p>An API endpoint might return the correct response in one scenario but break when multiple requests arrive simultaneously.</p><p>And a database query might work with ten records but become painfully slow with a million.</p><p>That’s why verification is so important.</p><p>Instead of asking Claude to implement something and stopping there, give it a way to test its own output.</p><p>For example, after implementing a Python function, use this prompt:</p><p>“Review the implementation and verify its behavior. Create unit tests covering normal inputs, empty values, invalid inputs, boundary conditions, and unexpected exceptions. Run the tests, inspect any failures, fix the underlying problems, and run the tests again.</p><p>Do not claim the implementation works unless the tests actually pass.”</p><p>This changes the workflow considerably.</p><p>Instead of receiving code and manually discovering every problem, Claude gets an opportunity to investigate failures and improve its implementation.</p><p>For Claude Code, you can take this further by asking it to:</p><ul><li>Run the project’s existing test suite.</li><li>Check formatting and linting.</li><li>Inspect the Git diff.</li><li>Identify unintended changes.</li><li>Review performance-sensitive sections.</li><li>Run relevant integration tests.</li></ul><p>One particularly useful practice is to make verification part of the task itself.</p><p>Don’t treat testing as something you’ll remember to do later.</p><p>Make it a required final step.</p><p>As Anthropic’s Claude Code team emphasizes in its power-user guidance, verification is one of the most impactful ways to improve the reliability of AI-generated work.</p><p><strong>Pro Tip:</strong> Never confuse code that Claude has written with code that Claude has verified. Those are two very different things.</p><h3>5. Turn Repeated Work Into Automated Workflows</h3><p>This is where things become particularly interesting.</p><p>Most people use AI to complete individual tasks.</p><p>Experienced developers eventually start thinking about entire workflows.</p><p>Consider a simple example.</p><p>Every morning, you need to:</p><ol><li>Check GitHub issues.</li><li>Review recent commits.</li><li>Identify unfinished tasks.</li><li>Summarize development progress.</li><li>Prepare a standup report.</li></ol><p>You could ask Claude to perform each task separately.</p><p>But why repeat the same instructions every morning?</p><p>With Claude Code, you can create reusable Skills for recurring workflows.</p><p>For example, you could create a /standup Skill that tells Claude to inspect recent Git commits and relevant project information, then generate a structured daily report.</p><p>A simplified Skill structure looks like this:</p><pre>.claude/<br>└── skills/<br>    └── standup/<br>        └── SKILL.md</pre><p>Inside SKILL.md, you might write:</p><pre>---<br>name: standup<br>description: Generate a daily development standup report.<br>---<br># Daily Standup Report<br>Review recent Git activity and summarize:<br>1. Work completed.<br>2. Important commits.<br>3. Current blockers.<br>4. Outstanding tasks.<br>5. Suggested priorities for today.<br>Keep the report concise and use bullet points.<br>Do not invent completed work or assume that<br>an unfinished task has been resolved.Now you have a reusable workflow rather than another prompt sitting somewhere in your chat history.</pre><p>You can extend this idea considerably.</p><p>For example:</p><ul><li>A /code-review workflow for reviewing changes.</li><li>A /documentation workflow for updating project documentation.</li><li>A /security-check workflow for identifying potential vulnerabilities.</li><li>A /techdebt workflow for finding duplicated code and maintainability issues.</li></ul><p>You can also explore Claude Code hooks when you need deterministic automation around specific events, such as running a formatter after certain operations.</p><p>The distinction is important: a Skill tells Claude how to perform a workflow, while a hook can automatically execute a predefined action when its configured event occurs.</p><p>That’s how you move from repeatedly asking AI for help to building a development environment that handles recurring work more consistently.</p><p>And honestly, that’s where the real productivity gains begin.</p><h3>One More Thing: Stop Treating Every AI Feature as Necessary</h3><p>There’s something I’ve noticed while experimenting with AI tools.</p><p>Whenever a new feature appears, developers immediately try to incorporate it into everything.</p><p>New agent? Add an agent.</p><p>New workflow framework? Rewrite the application.</p><p>New automation feature? Find something to automate.</p><p>But more features don’t automatically mean better results.</p><p>Sometimes a carefully written instruction is enough.</p><p>Sometimes a simple Python script is more reliable than an AI agent.</p><p>And sometimes the best solution is not using AI at all.</p><p>The trick is knowing which tool solves the actual problem.</p><p>As the old engineering principle goes:</p><blockquote><em>“Simplicity is the ultimate sophistication.” — Often attributed to Leonardo da Vinci</em></blockquote><p>I think that principle applies to AI development more than ever.</p><h3>Final Thoughts</h3><p>Claude is much more useful when you stop treating every conversation as an isolated interaction.</p><p>Reusable Skills reduce repetitive instructions.</p><p>Projects provide consistent context.</p><p>Planning improves implementation.</p><p>Verification makes AI-generated work more dependable.</p><p>And automated workflows help you stop repeating the same tasks.</p><p>None of these techniques requires you to become an AI researcher.</p><p>They simply require you to rethink how you use the tools already available.</p><p>My biggest takeaway is this:</p><p><strong>Don’t focus on getting Claude to answer faster. Focus on building a workflow that makes you ask for the same thing fewer times.</strong></p><p>That’s a much more valuable advantage than knowing a hundred clever prompts.</p><p>And it’s something you can start applying to your next project today.</p><p>What Claude workflow has saved you the most time? I’d love to hear about it in the comments.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=cdadb1091a25" width="1" height="1" alt=""><hr><p><a href="https://medium.com/codetodeploy/5-claude-tricks-that-give-you-an-unfair-advantage-most-people-still-use-the-old-way-cdadb1091a25">5 Claude Tricks That Give You an Unfair Advantage (Most People Still Use the Old Way)</a> was originally published in <a href="https://medium.com/codetodeploy">CodeToDeploy</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Prompting AI to Code Is Holding You Back. Here’s What I Do Instead.]]></title>
            <link>https://medium.com/codetodeploy/prompting-ai-to-code-is-holding-you-back-heres-what-i-do-instead-3f87432c3d74?source=rss-df9e89e9c50c------2</link>
            <guid isPermaLink="false">https://medium.com/p/3f87432c3d74</guid>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[math]]></category>
            <category><![CDATA[technology]]></category>
            <category><![CDATA[business]]></category>
            <category><![CDATA[programming]]></category>
            <dc:creator><![CDATA[Lubnaaly]]></dc:creator>
            <pubDate>Fri, 13 Nov 2026 13:55:28 GMT</pubDate>
            <atom:updated>2026-11-13T13:55:28.281Z</atom:updated>
            <content:encoded><![CDATA[<h4><em>After experimenting with AI-assisted development, I discovered a cleaner workflow that produces better code with fewer prompts, less debugging, and more control.</em></h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*S-GWT6AordN54DTC" /><figcaption>Photo by <a href="https://unsplash.com/@roozbeheslami?utm_source=medium&amp;utm_medium=referral">Roozbeh Eslami</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>For a long time, my AI coding workflow looked something like this:</p><p>Open ChatGPT.</p><p>Explain what I want to build.</p><p>Copy the generated code.</p><p>Paste it into my editor.</p><p>Run it.</p><p>Find an error.</p><p>Go back to ChatGPT.</p><p>Explain the error.</p><p>Copy the fixed code.</p><p>Repeat.</p><p>Sound familiar?</p><p>At first, this felt revolutionary. I could write Python scripts faster, explore unfamiliar libraries, and build features without spending hours searching through documentation.</p><p>But after using AI coding assistants for several projects, I noticed something.</p><p>I was spending almost as much time explaining my code to AI as I was actually developing it.</p><p>Sometimes even more.</p><p>The problem wasn’t that AI couldn’t write good code. The problem was how I was using it.</p><p>I was treating a powerful development assistant like a search engine that happened to understand Python.</p><p>Eventually, I changed my workflow.</p><p>Instead of writing endless prompts and asking AI to generate individual pieces of code, I started giving it access to the problem, the project structure, the requirements, and the expected behavior.</p><p>The difference was surprisingly significant.</p><p>Here’s what I do differently now.</p><h3>1. I Stop Writing Prompts and Start Defining the Problem</h3><p>One mistake I see developers make is immediately asking AI to generate code.</p><p>For example:</p><p>“Write a Python script that reads a CSV file, removes duplicates, and saves the result.”</p><p>There’s nothing particularly wrong with this prompt.</p><p>But it gives AI almost no information about the actual problem.</p><p>What happens if the CSV contains missing values?</p><p>Which columns determine whether a record is duplicated?</p><p>Should the original file be overwritten?</p><p>What happens when the file doesn’t exist?</p><p>A few missing details can turn a seemingly simple script into a collection of unexpected bugs.</p><p>Instead, I describe the problem before asking for an implementation.</p><p>For example:</p><p><strong>Project:</strong> CSV Data Cleaning Tool</p><p><strong>Objective:</strong> Build a reusable Python utility that cleans customer datasets.</p><p><strong>Requirements:</strong></p><ul><li>Accept CSV files as input.</li><li>Remove duplicate records based on email addresses.</li><li>Handle missing values without silently deleting important information.</li><li>Validate required columns.</li><li>Save the cleaned dataset to a separate output file.</li><li>Log how many records were removed or modified.</li></ul><p><strong>Constraints:</strong></p><ul><li>Use Pandas.</li><li>Keep the implementation modular.</li><li>Handle invalid input gracefully.</li><li>Include basic unit tests.</li></ul><p>Notice the difference?</p><p>The second approach gives AI something much more valuable than instructions.</p><p>It gives it context.</p><p>When AI understands what you’re trying to accomplish, it can make better implementation decisions instead of guessing what you meant.</p><p>I’ve learned that five minutes spent defining a problem can save considerably more time debugging an incomplete solution.</p><h3>2. I Give AI the Project Context Before Asking It to Change Anything</h3><p>This is probably one of the biggest improvements I’ve made.</p><p>Imagine joining an unfamiliar software project.</p><p>Would you immediately start changing files without understanding its architecture?</p><p>Probably not.</p><p>Yet developers often do exactly that with AI coding assistants.</p><p>They ask an AI tool to implement a feature without explaining:</p><ul><li>How the project is organized.</li><li>Which frameworks are being used.</li><li>Where business logic belongs.</li><li>How the database is structured.</li><li>Which coding conventions the project follows.</li><li>How existing components communicate.</li></ul><p>Then they wonder why the generated code doesn’t fit the application.</p><p>My approach is different.</p><p>Before asking AI to implement anything substantial, I ask it to inspect the existing codebase.</p><p>For example:</p><p>“Analyze this project and explain its architecture. Identify the main modules, important dependencies, entry points, database interactions, and existing coding conventions. Don’t modify any files yet.”</p><p>I want the AI to understand the project before touching it.</p><p>For larger projects, I also maintain a small AGENTS.md or equivalent project instruction file containing:</p><ul><li>Project overview.</li><li>Technology stack.</li><li>Directory structure.</li><li>Development commands.</li><li>Testing requirements.</li><li>Important architectural decisions.</li><li>Coding conventions.</li></ul><p>This prevents me from explaining the same things repeatedly.</p><p>It also makes the AI’s suggestions more consistent across different tasks.</p><p><strong>One useful principle:</strong> Don’t make AI rediscover information that your project can explain once.</p><h3>3. I Ask AI to Create a Plan Before Writing Code</h3><p>I used to think asking AI for a plan was unnecessary.</p><p>Why spend time discussing implementation when the model can generate code immediately?</p><p>That assumption didn’t age well.</p><p>On larger tasks, jumping directly into implementation often produces code that looks convincing but doesn’t fit the overall architecture.</p><p>For example, imagine building an AI-powered document processing application.</p><p>The project might require:</p><ol><li>PDF extraction.</li><li>Text cleaning.</li><li>Document chunking.</li><li>Embedding generation.</li><li>Vector storage.</li><li>Semantic retrieval.</li><li>LLM-based question answering.</li></ol><p>Asking AI to implement everything in one go is an invitation to unnecessary complexity.</p><p>Instead, I ask:</p><p>“Analyze the requirements and propose an implementation plan. Break the project into small tasks, identify the files that need modification, mention potential edge cases, and explain how each component will be tested. Don’t write code yet.”</p><p>This gives me an opportunity to review the architecture before implementation begins.</p><p>I can identify unnecessary dependencies, question questionable design decisions, and correct misunderstandings early.</p><p>It’s much cheaper to fix a bad plan than to rewrite 500 lines of generated code.</p><p>And here’s something worth remembering:</p><p><strong>AI should help you think through the implementation, not make every architectural decision on your behalf.</strong></p><h3>4. I Let AI Work on Small, Independent Tasks</h3><p>One of the most useful changes I’ve made is reducing the size of individual coding tasks.</p><p>I no longer ask:</p><p>“Build the entire backend for my application.”</p><p>Instead, I break the implementation into manageable pieces.</p><p>For example:</p><p><strong>Task 1:</strong> Create the document upload endpoint.</p><p><strong>Task 2:</strong> Implement PDF text extraction.</p><p><strong>Task 3:</strong> Add document validation.</p><p><strong>Task 4:</strong> Implement text chunking.</p><p><strong>Task 5:</strong> Integrate the embedding model.</p><p><strong>Task 6:</strong> Write tests for the retrieval functionality.</p><p>Each task has a clear responsibility and an expected result.</p><p>Why does this matter?</p><p>Because smaller tasks are easier to review, test, and debug.</p><p>If the PDF extraction module fails, I don’t need to investigate the entire backend.</p><p>I can focus on one component.</p><p>This also makes AI-generated changes easier to understand.</p><p>A useful coding assistant should make your development process more manageable, not turn your repository into a mystery.</p><h3>5. I Make AI Write Tests, Not Just Implementation Code</h3><p>This is where things get interesting.</p><p>Most developers use AI to generate functions.</p><p>Fewer consistently ask it to generate meaningful tests for those functions.</p><p>I’ve found that the second habit is just as important as the first.</p><p>Consider a simple Python function:</p><pre>def calculate_discount(price, discount):<br>    return price - (price * discount / 100)</pre><p>It works.</p><p>But what happens when:</p><ul><li>The price is negative?</li><li>The discount is greater than 100?</li><li>The input is a string?</li><li>The discount is zero?</li><li>The price is a floating-point value?</li></ul><p>The happy path tells us very little about how reliable a function actually is.</p><p>Now I ask AI to generate tests alongside the implementation.</p><p>For example:</p><pre>import pytest<br>def test_calculate_discount():<br>    assert calculate_discount(100, 10) == 90<br>    assert calculate_discount(200, 25) == 150<br>    assert calculate_discount(100, 0) == 100For production-oriented code, I’d also ask it to consider invalid inputs and define the expected behavior explicitly.</pre><p>Using pytest, I can automate these checks and quickly identify regressions when the implementation changes.</p><p>My preferred workflow is simple:</p><ol><li>Ask AI to implement the feature.</li><li>Ask it to identify important edge cases.</li><li>Generate tests for those cases.</li><li>Run the tests.</li><li>Review failures before accepting the changes.</li></ol><p>One important distinction: generated tests aren’t automatically correct. They can repeat the same misunderstanding as the implementation.</p><p>That’s why I review the expected behavior rather than blindly trusting a passing test suite.</p><p><strong>Pro Tip:</strong> Ask AI, “What are the five most likely ways this implementation could fail in production?” before considering a feature complete.</p><p>You might be surprised by what it catches.</p><h3>6. I Use AI to Review Code Before I Review It Myself</h3><p>Another habit that has saved me considerable time is asking AI to review its own implementation.</p><p>Not because AI is always better at finding bugs than an experienced developer.</p><p>It isn’t.</p><p>But it can provide another perspective, especially when I’m working with unfamiliar code.</p><p>After implementing a feature, I ask it to inspect the changes for:</p><ul><li>Incorrect assumptions.</li><li>Unnecessary complexity.</li><li>Missing error handling.</li><li>Security vulnerabilities.</li><li>Performance bottlenecks.</li><li>Duplicate logic.</li><li>Potential race conditions.</li><li>Missing tests.</li></ul><p>For example:</p><p>“Review the changes you just made as a senior Python engineer. Focus on correctness, maintainability, security, and edge cases. Don’t rewrite working code unnecessarily. Report issues with their severity and explain why each one matters.”</p><p>This is more useful than simply asking:</p><p>“Is this code good?”</p><p>The latter encourages generic feedback.</p><p>The former gives the review a purpose.</p><p>I also pay particular attention to database queries, API authentication, file handling, and user input validation.</p><p>These are areas where a small implementation mistake can create a surprisingly large problem.</p><p>Of course, AI reviews aren’t a substitute for code review, security testing, or human judgment.</p><p>Think of them as another layer of inspection.</p><h3>7. I Stop Copying Code Between Different Tools</h3><p>There was a time when my development workflow involved constantly switching between my browser, terminal, ChatGPT, and code editor.</p><p>I’d ask ChatGPT for a function, copy it into VS Code, run it, copy the error, and paste it back into the conversation.</p><p>It worked.</p><p>But it wasn’t particularly efficient.</p><p>Modern AI coding tools can work much closer to the actual development environment.</p><p>Depending on the tool, they can:</p><ul><li>Inspect project files.</li><li>Search through existing functions.</li><li>Edit multiple related files.</li><li>Run terminal commands.</li><li>Execute tests.</li><li>Explain errors.</li><li>Review changes.</li></ul><p>This changes the way I approach development.</p><p>Instead of asking AI to generate isolated code snippets, I can give it a task within the project itself.</p><p>For example:</p><p>“Implement pagination for the existing FastAPI endpoint. Follow the current project conventions, preserve backward compatibility, add input validation, and write tests. Run the relevant test suite and report any failures. Don’t modify unrelated files.”</p><p>The important part isn’t that AI can edit multiple files.</p><p>It’s that it can work with the actual project context and verify its changes.</p><p>Tools such as Cursor and Claude Code make this style of development possible, although their exact capabilities and workflows differ.</p><p>The less time I spend moving code between applications, the more time I can spend understanding what I’m building.</p><h3>8. I Make AI Explain Important Decisions</h3><p>There’s one thing I deliberately avoid.</p><p>Accepting code simply because it works.</p><p>A function can produce the correct output and still be poorly designed.</p><p>It might introduce unnecessary dependencies, duplicate existing functionality, or create performance problems that won’t appear until the application grows.</p><p>Whenever AI makes an important architectural decision, I ask it to explain why.</p><p>For example:</p><p>“Explain why you chose this approach instead of using a background task. What are the trade-offs, and under what circumstances would this implementation become inefficient?”</p><p>This is particularly useful when working with:</p><ul><li>Asynchronous Python.</li><li>Database connection management.</li><li>Background task processing.</li><li>Caching strategies</li><li>API architecture.</li><li>LLM orchestration.</li><li>Vector database retrieval.</li></ul><p>You don’t need to understand every generated line immediately.</p><p>But you should understand the important decisions that shape your application.</p><p>Otherwise, you’re not really developing software.</p><p>You’re supervising code generation.</p><p>And those are two very different things.</p><h3>The Workflow I Actually Follow Now</h3><p>After experimenting with different approaches, I’ve settled on a relatively simple process.</p><p>It looks like this:</p><p><strong>Step 1: Define the problem.</strong></p><p>Describe what needs to be built, who will use it, and what success looks like.</p><p><strong>Step 2: Provide project context.</strong></p><p>Let AI inspect the existing codebase, dependencies, architecture, and conventions.</p><p><strong>Step 3: Generate an implementation plan.</strong></p><p>Break the work into small, independent tasks.</p><p><strong>Step 4: Implement one task at a time.</strong></p><p>Let AI modify the relevant files instead of generating disconnected snippets.</p><p><strong>Step 5: Generate and run tests.</strong></p><p>Verify functionality, edge cases, and expected behavior.</p><p><strong>Step 6: Review the changes.</strong></p><p>Inspect the diff, question architectural decisions, and check for unnecessary modifications.</p><p><strong>Step 7: Document important decisions.</strong></p><p>Update project instructions or documentation when the architecture or development process changes.</p><p>The entire process is less about writing better prompts and more about creating a better development environment for AI.</p><p>That’s the distinction I wish I’d understood earlier.</p><h3>Final Thoughts</h3><p>AI hasn’t made software engineering effortless.</p><p>It has changed where the effort goes.</p><p>A few years ago, I spent more time writing boilerplate, searching documentation, and figuring out implementation details.</p><p>Today, AI can help with much of that work.</p><p>But the responsibility for understanding the problem, making architectural decisions, and verifying the result still belongs to the developer.</p><p>That’s why I no longer measure my AI coding workflow by how many prompts I can write or how quickly I can generate code.</p><p>I measure it by something much simpler:</p><p>How much reliable software can I build without creating unnecessary complexity?</p><p>The biggest improvement in my development process didn’t come from learning a better prompt.</p><p>It came from changing the way I work with AI.</p><p><strong>Stop treating AI like a machine that writes code on command. Start treating it like a development assistant that understands your project, follows your plan, and helps verify the result.</strong></p><p>You’ll spend less time prompting.</p><p>Less time debugging avoidable mistakes.</p><p>And considerably more time building things that actually work.</p><p>Because ultimately, the goal isn’t to make AI write more code.</p><p>It’s to make yourself a better developer.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3f87432c3d74" width="1" height="1" alt=""><hr><p><a href="https://medium.com/codetodeploy/prompting-ai-to-code-is-holding-you-back-heres-what-i-do-instead-3f87432c3d74">Prompting AI to Code Is Holding You Back. Here’s What I Do Instead.</a> was originally published in <a href="https://medium.com/codetodeploy">CodeToDeploy</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[I Thought I Understood AI Until I Started Building With It]]></title>
            <link>https://medium.com/codetodeploy/i-thought-i-understood-ai-until-i-started-building-with-it-8cfacf62d028?source=rss-df9e89e9c50c------2</link>
            <guid isPermaLink="false">https://medium.com/p/8cfacf62d028</guid>
            <category><![CDATA[python]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[technology]]></category>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[programming]]></category>
            <dc:creator><![CDATA[Lubnaaly]]></dc:creator>
            <pubDate>Tue, 03 Nov 2026 05:16:20 GMT</pubDate>
            <atom:updated>2026-11-07T17:18:46.411Z</atom:updated>
            <content:encoded><![CDATA[<h4><em>Reading about AI is easy. Building something useful with it taught me lessons I couldn’t learn from tutorials.</em></h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*WhzvTzBv6bm50oNq" /><figcaption>Photo by <a href="https://unsplash.com/@steve_j?utm_source=medium&amp;utm_medium=referral">Steve A Johnson</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>For a long time, I thought I had a pretty good understanding of AI.</p><blockquote><em>💥 </em><strong><em>Master AI &amp; Tech Skills </em></strong><br> 🔥 Up to 5<strong>0% OFF</strong> — Limited-Time Offer<br><a href="https://trk.udemy.com/zz4NBO"><em>👉 </em><strong><em>Enroll Now &amp; Start Learning</em></strong></a></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ITogvtC6mF2BUF35Qsd9pg.png" /></figure><p>I knew how large language models worked at a high level. I had experimented with different prompts, explored popular Python libraries, tested APIs, and built a few small scripts.</p><p>I could explain what RAG was, why embeddings mattered, and how AI agents were supposed to work.</p><p>Then I started building actual AI applications.</p><p>And things got interesting.</p><p>A prompt that worked perfectly in a notebook suddenly produced inconsistent results in production. An API call that took two seconds during testing occasionally took fifteen. A model that seemed intelligent when answering simple questions struggled with a document containing messy formatting.</p><p>I quickly realized something:</p><p><strong>Understanding how AI works and knowing how to build reliable AI software are two very different skills.</strong></p><p>The gap between them is where most of the interesting engineering happens.</p><p>Here are seven lessons I learned after moving beyond AI tutorials and starting to build applications that solve real problems.</p><h3>1. Prompt Engineering Is Easy Until You Need Consistency</h3><p>One of the first things I learned was that getting an LLM to produce a good answer is relatively easy.</p><p>Getting it to produce a consistently good answer is another story.</p><p>Consider a simple project: extracting information from invoices.</p><p>You provide an invoice and ask an LLM to return:</p><ul><li>Invoice number</li><li>Customer name</li><li>Invoice date</li><li>Total amount</li><li>Tax amount</li></ul><p>During initial testing, everything looks great.</p><p>Then you try another invoice.</p><p>The model returns the date in a different format. One field is missing. The total amount includes a currency symbol when your application expects a number.</p><p>Your Python code breaks.</p><p>This is where I started appreciating structured outputs and validation.</p><p>Instead of treating an LLM response as trustworthy data, I began treating it as an untrusted input that needed validation.</p><p>Libraries such as Pydantic make this much easier.</p><p>For example:</p><pre>from pydantic import BaseModel<br>from datetime import date<br>class Invoice(BaseModel):<br>    invoice_number: str<br>    customer_name: str<br>    invoice_date: date<br>    total_amount: float<br>    tax_amount: float<br>invoice_data = {<br>    &quot;invoice_number&quot;: &quot;INV-1024&quot;,<br>    &quot;customer_name&quot;: &quot;John Smith&quot;,<br>    &quot;invoice_date&quot;: &quot;2026-10-15&quot;,<br>    &quot;total_amount&quot;: 1250.50,<br>    &quot;tax_amount&quot;: 150.00<br>}<br>invoice = Invoice(**invoice_data)<br>print(invoice.total_amount)Now, instead of blindly accepting whatever the model returns, I have a clear contract for the data my application expects.</pre><p>For actual LLM integrations, you can combine Pydantic with provider-supported structured output features and still validate the result before using it.</p><p>The important lesson?</p><p><strong>A good AI application doesn’t just generate intelligent responses. It makes those responses usable by ordinary software.</strong></p><h3>2. The Model Is Not Always the Problem</h3><p>When an AI application produces an incorrect answer, our first instinct is usually to blame the model.</p><p>I used to do the same.</p><p>If a chatbot couldn’t answer a question about a PDF, I would experiment with different prompts, adjust temperatures, or switch models.</p><p>Sometimes that helped.</p><p>Often, it didn’t.</p><p>The problem was somewhere else.</p><p>Imagine building a document question-answering application using Retrieval-Augmented Generation (RAG).</p><p>The workflow looks something like this:</p><ol><li>Extract text from documents.</li><li>Divide the text into smaller chunks.</li><li>Generate embeddings.</li><li>Retrieve relevant chunks.</li><li>Pass those chunks to an LLM.</li><li>Generate an answer.</li></ol><p>Now imagine the user asks:</p><p><em>“What was the company’s revenue in 2024?”</em></p><p>But your retrieval system returns a paragraph about the company’s 2023 revenue.</p><p>The LLM may generate a perfectly convincing answer.</p><p>It will still be wrong.</p><p>Changing GPT to Claude won’t magically fix poor retrieval.</p><p>This taught me to debug AI applications as complete pipelines rather than isolated model calls.</p><p>I now pay attention to:</p><ul><li>Whether the source documents were extracted correctly.</li><li>Whether chunk boundaries preserve useful context.</li><li>Whether the embedding model captures the meaning of the query.</li><li>Whether retrieval returns relevant information.</li><li>Whether the final answer is supported by the retrieved content.</li></ul><p>A useful tool here is sentence-transformers.</p><pre>from sentence_transformers import SentenceTransformer<br>from sklearn.metrics.pairwise import cosine_similarity<br>model = SentenceTransformer(&quot;all-MiniLM-L6-v2&quot;)<br>documents = [<br>    &quot;The company generated $2 million in revenue during 2024.&quot;,<br>    &quot;The company hired 150 new employees in 2023.&quot;,<br>    &quot;The organization expanded into three new markets.&quot;<br>]<br>query = &quot;What was the company&#39;s revenue in 2024?&quot;<br>embeddings = model.encode(documents)<br>query_embedding = model.encode([query])<br>scores = cosine_similarity(<br>    query_embedding,<br>    embeddings<br>)[0]<br>best_match = scores.argmax()<br>print(documents[best_match])This is a simple demonstration of semantic retrieval, not a complete production RAG system. Real applications also need thoughtful chunking, metadata filtering, evaluation, and often reranking.</pre><p>But it illustrates an important point.</p><p>Sometimes the best way to improve an AI application isn’t to use a smarter model.</p><p>It’s to give the model better information.</p><h3>3. AI APIs Introduce Problems That Ordinary Python Scripts Don’t</h3><p>Writing a Python script that calls an AI API is straightforward.</p><p>Building one that handles thousands of requests reliably requires a different mindset.</p><p>I learned this when experimenting with workflows that involved processing multiple documents.</p><p>Initially, I simply looped through the files and sent requests to an LLM.</p><p>Something like this:</p><pre>for document in documents:<br>    response = client.chat.completions.create(<br>        model=&quot;gpt-4o-mini&quot;,<br>        messages=[<br>            {<br>                &quot;role&quot;: &quot;user&quot;,<br>                &quot;content&quot;: f&quot;Summarize this: {document}&quot;<br>            }<br>        ]<br>    )</pre><p>It works.</p><p>Until you have hundreds or thousands of documents.</p><p>Then you start encountering:</p><ul><li>Rate limits.</li><li>Network timeouts.</li><li>Temporary provider errors.</li><li>Unexpected API costs.</li><li>Failed requests.</li><li>Long execution times.</li></ul><p>A script that works perfectly with five documents might become unreliable with five thousand.</p><p>This is why I started paying more attention to retry mechanisms, batching, concurrency, and background processing.</p><p>For example, exponential backoff is a useful technique for handling temporary failures.</p><pre>import time<br>import random<br>def retry_request(func, retries=5):<br>    for attempt in range(retries):<br>        try:<br>            return func()<br>        except Exception:<br>            if attempt == retries - 1:<br>                raise<br>            delay = (2 ** attempt) + random.random()<br>            time.sleep(delay)</pre><p>This is a simplified example. In production, retries should be limited to retryable errors, respect provider rate-limit instructions, and avoid blindly repeating non-idempotent operations.</p><p>The bigger lesson is that AI applications need the same reliability engineering as any other backend system.</p><p>An impressive model cannot compensate for an unreliable application around it.</p><h3>4. Data Preparation Matters More Than I Expected</h3><p>When people discuss AI development, they usually talk about models, agents, frameworks, and vector databases.</p><p>Data preparation doesn’t get nearly as much attention.</p><p>But it should.</p><p>I discovered this while experimenting with document-processing applications.</p><p>A PDF might contain:</p><ul><li>Tables with inconsistent formatting.</li><li>Scanned pages.</li><li>Repeated headers and footers.</li><li>Broken text extraction.</li><li>Unusual Unicode characters.</li><li>Important information inside images.</li></ul><p>Simply extracting text doesn’t mean you’ve extracted useful information.</p><p>For example, a PDF table might visually look like this:</p><p>ProductQuantityPriceKeyboard10500Mouse15200</p><p>But after text extraction, the content might become a poorly structured sequence of words and numbers.</p><p>The model then has to guess which value belongs to which column.</p><p>That’s unnecessary work.</p><p>For PDF processing, I frequently find PyMuPDF useful.</p><p>For tabular data, Pandas remains one of the most practical tools in my Python toolbox.</p><pre>import fitz<br>pdf = fitz.open(&quot;report.pdf&quot;)<br>for page_number, page in enumerate(pdf):<br>    text = page.get_text(&quot;text&quot;)<br>    print(f&quot;Page {page_number + 1}&quot;)<br>    print(text)This gives you a starting point for extracting document content.</pre><p>For more complicated documents, you may need table extraction, OCR, layout detection, or image processing.</p><p>The interesting part is that improving preprocessing can sometimes produce better results than changing the underlying LLM.</p><p><strong>Garbage in, garbage out isn’t an outdated programming principle. AI just makes it more expensive.</strong></p><h3>5. Not Every AI Problem Needs an AI Agent</h3><p>This was one of the most useful lessons I learned.</p><p>Whenever a new AI concept becomes popular, developers naturally want to use it everywhere.</p><p>Agents are a good example.</p><p>An AI agent can use tools, make decisions, perform multiple steps, and coordinate actions to accomplish a goal.</p><p>That’s powerful.</p><p>But does every application need one?</p><p>Consider a simple email classification system.</p><p>You receive an email and want to categorize it as:</p><ul><li>Sales</li><li>Support</li><li>Billing</li><li>General inquiry</li></ul><p>You could build an agent that reasons about the email, selects tools, decides what to do next, and manages a workflow.</p><p>Or you could send the email to an LLM with a structured classification prompt.</p><p>Which approach would I choose?</p><p>The second one.</p><p>It is simpler, cheaper to maintain, and easier to test.</p><p>Agents become more useful when a task genuinely requires multiple decisions, tool interactions, or adaptive execution.</p><p>For more complicated workflows, frameworks such as LangGraph can help manage state, branching, retries, and human approval steps.</p><p>But I wouldn’t introduce that complexity without a reason.</p><p>A useful rule I’ve adopted:</p><blockquote><em>Don’t introduce an agent when a function can solve the problem.</em></blockquote><p>Sometimes the most advanced engineering decision is choosing not to use advanced technology.</p><h3>6. Evaluating AI Applications Is Harder Than Testing Normal Python Code</h3><p>Traditional Python testing is relatively straightforward.</p><p>You provide an input, execute a function, and compare the output with an expected result.</p><p>For example:</p><pre>def add(a, b):<br>    return a + b<br>assert add(5, 10) == 15AI applications are different.</pre><p>The same prompt can produce slightly different answers. Two responses can communicate the same information using completely different words.</p><p>So, how do you test them?</p><p>You need to evaluate more than whether the output matches an exact string.</p><p>For an AI-powered document assistant, I would start with a small evaluation dataset containing:</p><ul><li>Questions with known answers.</li><li>Relevant source documents.</li><li>Expected supporting passages.</li><li>Questions that cannot be answered from the documents.</li></ul><p>Then evaluate different parts of the system.</p><p><strong>Retrieval evaluation:</strong> Did the application retrieve the correct information?</p><p><strong>Answer evaluation:</strong> Did the generated response accurately answer the question?</p><p><strong>Grounding evaluation:</strong> Was the answer supported by the retrieved sources?</p><p><strong>Reliability evaluation:</strong> Does the system behave consistently across repeated tests?</p><p>Tools such as Ragas and DeepEval can help evaluate RAG and LLM applications.</p><p>I also recommend keeping a collection of real failure cases.</p><p>Whenever the application produces an incorrect answer, save the input, retrieved context, and output.</p><p>That collection gradually becomes one of your most valuable testing resources.</p><p>Because fixing a bug once is good.</p><p>Making sure it doesn’t return six months later is better.</p><h3>7. The Best AI Applications Are Usually Boring Behind the Scenes</h3><p>This might sound strange, but the more AI applications I explored, the less impressed I became by complicated demonstrations.</p><p>A chatbot that can answer questions about anything looks exciting.</p><p>An application that automatically processes 2,000 invoices, validates the extracted information, flags suspicious entries, and sends a report to the accounting team is much more interesting to me.</p><p>Why?</p><p>Because the second application solves a specific problem.</p><p>It has a clear purpose.</p><p>It saves time.</p><p>And someone can actually use it.</p><p>When I approach a new AI project today, I usually think about the entire workflow:</p><ul><li>FastAPI for exposing functionality through an API.</li><li>Pydantic for validating structured data.</li><li>Pandas for handling datasets.</li><li>PyMuPDF for extracting PDF content.</li><li>Sentence Transformers for generating embeddings.</li><li>Celery for background processing.</li><li>PostgreSQL for persistent storage.</li><li>An LLM API for tasks that genuinely require language understanding.</li></ul><p>Not every project needs all these libraries.</p><p>The point is to choose tools according to the problem rather than forcing every new technology into the architecture.</p><p>A successful AI application doesn’t have to look complicated.</p><p>It has to work reliably.</p><h3>What Changed the Way I Think About AI Development?</h3><p>Looking back, the biggest change wasn’t learning another framework or discovering a new model.</p><p>It was changing the questions I asked.</p><p>Earlier, I would ask:</p><p><em>“Which model should I use?”</em></p><p>Now I ask:</p><p><em>“What exactly am I trying to automate?”</em></p><p>Earlier, I would ask:</p><p><em>“How can I make this prompt better?”</em></p><p>Now I also ask:</p><p><em>“Is the model receiving the right information in the first place?”</em></p><p>Earlier, I was impressed when an AI demo worked once.</p><p>Now I want to know what happens after 1,000 requests.</p><p>That shift has made me approach AI development much more like software engineering.</p><p>And honestly, that’s what building useful AI applications requires.</p><h3>Final Thoughts</h3><p>I still enjoy exploring new AI models, libraries, and frameworks.</p><p>There’s always something interesting happening in the ecosystem.</p><p>But I’ve stopped measuring my AI knowledge by how many tools I know or how many tutorials I’ve completed.</p><p>I measure it by what I can actually build.</p><p>Can I turn an unstructured document collection into a searchable knowledge base?</p><p>Can I automate a repetitive business process without creating a maintenance nightmare?</p><p>Can I build an application that handles unexpected inputs, API failures, and imperfect data?</p><p>Those are much more interesting questions.</p><p>If you’re learning AI, my advice is simple.</p><p>Don’t spend all your time reading about what AI can do.</p><p>Pick a problem you’ve personally experienced and build something that solves it.</p><p>Start small. Make mistakes. Debug the unexpected behavior. Improve the workflow.</p><p>You’ll probably learn more from one imperfect project than from ten perfectly polished tutorials.</p><p>Because there’s a big difference between knowing how AI works and knowing how to make it work.</p><p><strong>And that difference only becomes clear when you start building.</strong></p><h3>Thank you for being a part of the community</h3><p><em>Before you go:</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*d9QTaaaxboQP_gKSLedW_w.png" /></figure><p>👉 Be sure to <strong>clap</strong> and <strong>follow</strong> the writer ️👏<strong>️️</strong></p><p>👉 Follow us: <a href="https://medium.com/codetodeploy"><strong>Medium</strong></a></p><p><strong>Disclosure:</strong> This post includes affiliate and partnership links.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8cfacf62d028" width="1" height="1" alt=""><hr><p><a href="https://medium.com/codetodeploy/i-thought-i-understood-ai-until-i-started-building-with-it-8cfacf62d028">I Thought I Understood AI Until I Started Building With It</a> was originally published in <a href="https://medium.com/codetodeploy">CodeToDeploy</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[I Stopped Following Every Web Development Trend — and Became a Better Developer]]></title>
            <link>https://medium.com/codetodeploy/i-stopped-following-every-web-development-trend-and-became-a-better-developer-0de480aa7849?source=rss-df9e89e9c50c------2</link>
            <guid isPermaLink="false">https://medium.com/p/0de480aa7849</guid>
            <category><![CDATA[python]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[web-development]]></category>
            <dc:creator><![CDATA[Lubnaaly]]></dc:creator>
            <pubDate>Mon, 02 Nov 2026 06:02:17 GMT</pubDate>
            <atom:updated>2026-11-02T17:29:40.494Z</atom:updated>
            <content:encoded><![CDATA[<h4>After years of chasing new frameworks, tools, and JavaScript libraries, I realized that becoming a better developer had very little to do with keeping up with everything new.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*mnLhvjcjPmKtKFlh" /><figcaption>Photo by <a href="https://unsplash.com/@jakubzerdzicki?utm_source=medium&amp;utm_medium=referral">Jakub Żerdzicki</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>A few years ago, I had a habit that I thought made me a good developer.</p><blockquote><em>💥 </em><strong><em>Master AI &amp; Tech Skills in 3 Months!</em></strong><br>💰 One Subscription. Multiple Courses. More Savings.<br>🔥 Get Up to 50% OFF | ⏰ Limited-Time Offer<br><a href="https://trk.udemy.com/zz4NBO"><em>👉 </em><strong><em>Enroll Now &amp; Start Learning</em></strong></a></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ITogvtC6mF2BUF35Qsd9pg.png" /></figure><p>Whenever a new JavaScript framework appeared, I wanted to try it.</p><p>A new CSS library? Installed.</p><p>A new build tool? Tested.</p><p>A new frontend framework promising better performance and developer experience? I was already reading its documentation.</p><p>My browser was full of documentation tabs, GitHub repositories, tutorials, and blog posts about the next big thing in web development.</p><p>I thought staying updated meant becoming a better programmer.</p><p>I was wrong.</p><p>The more technologies I learned, the more I realized that I wasn’t necessarily becoming better at building software. I was becoming better at learning new tools.</p><p>There’s a difference.</p><p>Eventually, I started asking myself a different question:</p><p><strong>What if I spent less time learning every new technology and more time understanding the technologies I already used?</strong></p><p>That small change completely transformed my approach to web development.</p><p>Here are seven lessons I learned after stepping away from the endless cycle of web development trends.</p><h3>1. I Stopped Switching Frameworks Just Because Something New Appeared</h3><p>For a long time, I treated frameworks like smartphones.</p><p>Every new release made me wonder whether I was missing something.</p><p>React was popular, then Vue caught my attention. Next.js introduced interesting features, and other frameworks started promising faster rendering and better developer experiences.</p><p>I kept experimenting.</p><p>There was nothing inherently wrong with that. Exploring different frameworks helped me understand how modern frontend development worked.</p><p>The problem was that I rarely stayed with one technology long enough to understand its deeper concepts.</p><p>I knew how to create components, but I wasn’t always thinking carefully about component boundaries.</p><p>I knew how to fetch data, but I didn’t fully understand caching strategies, request waterfalls, or server-side rendering trade-offs.</p><p>I could configure a project, but maintaining it over time was a different story.</p><p>So I changed my approach.</p><p>Instead of switching frameworks whenever something new appeared, I started evaluating technologies based on actual project requirements.</p><p>For example:</p><ul><li>Does the project need server-side rendering?</li><li>Is client-side interactivity essential?</li><li>How complicated will the routing become?</li><li>Does the team already understand the framework?</li><li>Will the technology remain maintainable as the application grows?</li></ul><p>These questions became more valuable than GitHub stars or social media discussions.</p><p><strong>The best framework isn’t necessarily the newest one. It’s the one that solves your problem without introducing unnecessary complexity.</strong></p><h3>2. I Started Understanding JavaScript Instead of Memorizing Framework APIs</h3><p>This was probably one of the biggest improvements in my development journey.</p><p>Frameworks make many things easier.</p><p>However, they can also hide the underlying concepts.</p><p>For example, you can use React for years without properly understanding:</p><ul><li>JavaScript closures</li><li>The event loop</li><li>Promises and asynchronous execution</li><li>Prototypes and inheritance</li><li>The difference between shallow and deep copying</li><li>How references work in JavaScript</li><li>Memory management</li></ul><p>But eventually, these concepts become important.</p><p>I remember spending time debugging unexpected behavior in a React component. The problem wasn’t really React. It was related to how JavaScript closures captured variables.</p><p>Understanding closures made the problem much easier to solve.</p><p>The same thing happens with asynchronous JavaScript.</p><p>If you understand how the event loop works, debugging unexpected execution order becomes significantly easier.</p><p>For example:</p><pre>console.log(&quot;Start&quot;);<br>setTimeout(() =&gt; {<br>    console.log(&quot;Timeout&quot;);<br>}, 0);<br>Promise.resolve().then(() =&gt; {<br>    console.log(&quot;Promise&quot;);<br>});<br>console.log(&quot;End&quot;);The output is:</pre><pre>Start<br>End<br>Promise<br>Timeout</pre><p>Why?</p><p>Because synchronous code executes first, promise callbacks are processed through the microtask queue, and timer callbacks are processed through the task queue.</p><p>Understanding this behavior is much more valuable than memorizing another framework method.</p><p>Frameworks change.</p><p>JavaScript fundamentals remain useful across almost every frontend project.</p><h3>3. I Stopped Collecting CSS Frameworks and Learned CSS Properly</h3><p>I’ll admit something.</p><p>There was a time when I thought learning another CSS framework would solve most of my styling problems.</p><p>Bootstrap made layouts easier.</p><p>Tailwind CSS made utility-based styling convenient.</p><p>Component libraries helped me build interfaces faster.</p><p>But I eventually noticed something.</p><p>Whenever a design became slightly complicated, I still needed to understand CSS.</p><p>Responsive layouts, unexpected overflow, alignment issues, stacking contexts, and complex positioning didn’t magically disappear because I was using a framework.</p><p>So I went back to the fundamentals.</p><p>I spent more time understanding:</p><ul><li>Flexbox</li><li>CSS Grid</li><li>Positioning and stacking contexts</li><li>Responsive design</li><li>Container queries</li><li>CSS custom properties</li><li>Specificity and inheritance</li></ul><p>One particularly useful concept was CSS Grid.</p><p>Before understanding it properly, I often used nested flex containers to build layouts that were essentially two-dimensional.</p><p>Grid made those layouts considerably easier to reason about.</p><p>For example:</p><pre>.dashboard {<br>    display: grid;<br>    grid-template-columns: 250px 1fr;<br>    grid-template-rows: 70px 1fr;<br>    min-height: 100vh;<br>}<br>.sidebar {<br>    grid-row: 1 / 3;<br>}<br>.header {<br>    grid-column: 2;<br>}<br>.main-content {<br>    grid-column: 2;<br>}This creates a simple dashboard structure without introducing unnecessary layout wrappers.</pre><p>I still use CSS frameworks when they make sense.</p><p>But I no longer expect them to replace an understanding of CSS.</p><p><strong>A framework should accelerate your work, not become a substitute for knowing what you’re doing.</strong></p><h3>4. I Became More Interested in Performance Than Popularity</h3><p>Something interesting happens when you stop chasing trends.</p><p>You start noticing problems that aren’t particularly exciting but matter enormously in real applications.</p><p>Performance is one of them.</p><p>A website can look beautiful, use the latest framework, and still provide a terrible user experience.</p><p>Slow loading, excessive JavaScript, layout shifts, and unnecessary network requests can make even a well-designed application frustrating.</p><p>I started paying more attention to performance measurements.</p><p>Instead of guessing whether a website was fast, I used tools such as:</p><ul><li>Lighthouse</li><li>Chrome DevTools</li><li>WebPageTest</li><li>Chrome Performance Panel</li><li>PageSpeed Insights</li></ul><p>I also became more familiar with Core Web Vitals.</p><p>Three particularly important metrics are:</p><p><strong>Largest Contentful Paint (LCP):</strong> Measures how quickly the largest visible content element loads.</p><p><strong>Interaction to Next Paint (INP):</strong> Measures how responsive a page feels when users interact with it.</p><p><strong>Cumulative Layout Shift (CLS):</strong> Measures unexpected visual movement during page loading.</p><p>These metrics helped me think beyond whether my code worked.</p><p>I started asking whether the application felt good to use.</p><p>Sometimes the biggest performance improvement wasn’t switching frameworks.</p><p>It was compressing images, removing unnecessary JavaScript, reducing third-party scripts, or fixing an inefficient API request.</p><p>That’s not particularly glamorous.</p><p>But users don’t care how fashionable your tech stack is.</p><p>They care about how quickly your website works.</p><h3>5. I Started Writing Code That Other Developers Could Understand</h3><p>When you’re learning web development, it’s easy to focus on making something work.</p><p>If the button responds, the API returns data, and the page renders correctly, the task feels complete.</p><p>But professional software requires more than that.</p><p>Code needs to be understandable, testable, and maintainable.</p><p>I gradually became more conscious of things like:</p><ul><li>Meaningful variable and function names</li><li>Smaller, focused components</li><li>Reusable logic</li><li>Proper error handling</li><li>Consistent project structure</li><li>Type safety with TypeScript</li><li>Automated testing</li></ul><p>TypeScript, in particular, changed how I approached larger applications.</p><p>Consider this example:</p><pre>type User = {<br>    id: number;<br>    name: string;<br>    email: string;<br>    isActive: boolean;<br>};<br>function getUserDetails(user: User): string {<br>    return `${user.name} - ${user.email}`;<br>}Without explicit types, it’s easier for incorrect data structures to travel through an application unnoticed.</pre><p>TypeScript helps catch many of these mistakes during development rather than waiting for them to appear at runtime.</p><p>Of course, TypeScript doesn’t eliminate every possible bug.</p><p>But it makes assumptions more visible.</p><p>And that’s valuable.</p><p>I also learned that reusable code isn’t automatically good code.</p><p>Sometimes developers create complicated abstractions to avoid repeating just two or three lines.</p><p>I’ve done that myself.</p><p>Now I prefer straightforward code over clever abstractions that require an explanation every time someone opens the file.</p><h3>6. I Stopped Ignoring Backend Development</h3><p>For a while, I approached web development primarily from the frontend.</p><p>My main concerns were components, layouts, interactions, and making interfaces look right.</p><p>But real applications don’t exist only in the browser.</p><p>They communicate with APIs, interact with databases, authenticate users, process information, and handle failures.</p><p>Understanding the backend made me a considerably more capable frontend developer.</p><p>I started paying more attention to:</p><ul><li>REST API design</li><li>HTTP methods and status codes</li><li>Authentication and authorization</li><li>Database relationships</li><li>API pagination</li><li>Caching</li><li>Input validation</li><li>Error handling</li></ul><p>One thing I particularly recommend understanding is the difference between authentication and authorization.</p><p>Authentication answers:</p><p><em>Who are you?</em></p><p>Authorization answers:</p><p><em>What are you allowed to do?</em></p><p>They sound similar, but confusing them can introduce serious security problems.</p><p>For example, hiding an administrative button in the frontend doesn’t prevent an unauthorized user from calling the underlying API directly.</p><p>Permissions must also be enforced on the server.</p><p>This is the kind of detail that separates building an interface from building a reliable application.</p><p>You don’t necessarily need to become a backend specialist.</p><p>But understanding what happens behind your frontend makes your architectural decisions much better.</p><h3>7. I Started Building Complete Projects Instead of Collecting Tutorials</h3><p>This was the final change that brought everything together.</p><p>I used to save tutorials constantly.</p><p>A new React course, a Next.js project, a CSS animation tutorial, or an interesting GitHub repository.</p><p>My learning list kept growing.</p><p>Unfortunately, my completed projects didn’t grow at the same speed.</p><p>So I started following a different rule.</p><p>Whenever I learned something new, I tried to use it in a real project.</p><p>Not another tutorial project that disappeared after two days.</p><p>Something that solved an actual problem.</p><p>For example, instead of simply learning how authentication works, I built an application with:</p><ul><li>User registration and login</li><li>Password hashing</li><li>Protected routes</li><li>Role-based permissions</li><li>Form validation</li><li>Error handling</li><li>Database integration</li></ul><p>Instead of watching another performance optimization tutorial, I took an existing application and measured its performance before and after making improvements.</p><p>This approach taught me things tutorials rarely cover.</p><p>What happens when an API fails?</p><p>What if a user submits the same form twice?</p><p>What if the database returns incomplete information?</p><p>What if a component receives unexpected data?</p><p>These problems force you to think like an engineer rather than someone following instructions.</p><p>And that’s where real development experience comes from.</p><h3>A Simple Rule I Now Follow When Learning New Technologies</h3><p>I haven’t stopped exploring new tools.</p><p>I still enjoy experimenting with frameworks, libraries, and development workflows.</p><p>The difference is that I no longer treat every new release as something I must learn immediately.</p><p>Before adopting a technology, I ask myself:</p><ol><li>What problem does it solve?</li><li>Is that problem actually present in my project?</li><li>Does it offer a meaningful improvement over my current approach?</li><li>How difficult will it be to maintain?</li><li>Will learning it improve my understanding of software development?</li></ol><p>If I can’t answer these questions, I usually don’t rush into adopting it.</p><p>A useful rule I learned along the way:</p><blockquote><em>“The more you know, the more you realize what you don’t need to know.”</em></blockquote><p>You don’t have to master every framework, library, or development tool to become an excellent developer.</p><p>You need to understand the fundamentals, recognize problems, and know how to choose the right solution.</p><h3>Final Thoughts</h3><p>Web development moves incredibly fast.</p><p>A framework that everyone discusses today might receive considerably less attention next year.</p><p>A new library might become popular overnight.</p><p>And another tool will probably promise to revolutionize the way we build websites.</p><p>That’s fine.</p><p>Technology should evolve.</p><p>But your learning strategy shouldn’t depend entirely on what’s trending.</p><p>I became a better developer when I stopped trying to learn everything and started focusing on understanding things deeply.</p><p>I spent more time debugging, reading documentation, improving existing projects, understanding browser behavior, and writing code that other developers could actually maintain.</p><p>Ironically, I learned more when I started following fewer trends.</p><p>My advice?</p><p>Don’t spend every weekend rebuilding your tech stack because someone on the internet discovered a new framework.</p><p>Pick a problem.</p><p>Build something useful.</p><p>Understand why it works.</p><p>And when something breaks, resist the temptation to immediately install another library.</p><p>Sometimes the best upgrade to your development workflow isn’t a new technology.</p><p><strong>It’s becoming better at using the technologies you already have.</strong></p><h3>Thank you for being a part of the community</h3><p><em>Before you go:</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*d9QTaaaxboQP_gKSLedW_w.png" /></figure><p>👉 Be sure to <strong>clap</strong> and <strong>follow</strong> the writer ️👏<strong>️️</strong></p><p>👉 Follow us: <a href="https://medium.com/codetodeploy"><strong>Medium</strong></a></p><p><strong>Disclosure:</strong> This post includes affiliate and partnership links.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0de480aa7849" width="1" height="1" alt=""><hr><p><a href="https://medium.com/codetodeploy/i-stopped-following-every-web-development-trend-and-became-a-better-developer-0de480aa7849">I Stopped Following Every Web Development Trend — and Became a Better Developer</a> was originally published in <a href="https://medium.com/codetodeploy">CodeToDeploy</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[My Website Worked Perfectly Locally — Then Everything Broke in Production]]></title>
            <link>https://medium.com/codetodeploy/my-website-worked-perfectly-locally-then-everything-broke-in-production-0315a611f62b?source=rss-df9e89e9c50c------2</link>
            <guid isPermaLink="false">https://medium.com/p/0315a611f62b</guid>
            <category><![CDATA[web-development]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[data-science]]></category>
            <dc:creator><![CDATA[Lubnaaly]]></dc:creator>
            <pubDate>Mon, 02 Nov 2026 05:57:54 GMT</pubDate>
            <atom:updated>2026-11-02T13:28:34.423Z</atom:updated>
            <content:encoded><![CDATA[<h4>The deployment mistakes, environment differences, and hidden configuration issues that taught me to build more reliable Python applications.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*BPVHFxeNL4YLfRxB" /><figcaption>Photo by <a href="https://unsplash.com/@danielkorpai?utm_source=medium&amp;utm_medium=referral">Daniel Korpai</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>read here.</p><p>I still remember deploying a Python application that had worked flawlessly on my local machine.</p><blockquote><em>💥 </em><strong><em>Master AI &amp; Tech Skills in 3 Months!</em></strong><br>💰 One Subscription. Multiple Courses. More Savings.<br>🔥 Get Up to 50% OFF | ⏰ Limited-Time Offer<br><a href="https://trk.udemy.com/zz4NBO"><em>👉 </em><strong><em>Enroll Now &amp; Start Learning</em></strong></a></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ITogvtC6mF2BUF35Qsd9pg.png" /></figure><p>Every endpoint responded correctly.</p><p>The database queries were fast.</p><p>The frontend loaded without errors.</p><p>Even my automated tests were passing.</p><p>I pushed the code, deployed the application, and waited for everything to come online.</p><p>Then the errors started.</p><p>The API couldn’t connect to the database. Some environment variables were missing. A few requests returned unexpected errors, and a feature that worked perfectly on my laptop suddenly stopped working.</p><p>Nothing had changed in the business logic.</p><p>At least, that’s what I thought.</p><p>After spending hours debugging the application, I realized something important:</p><p><strong>Writing code that works locally and building software that works reliably in production are two very different skills.</strong></p><p>Over the years, I’ve learned that many deployment failures aren’t caused by complicated bugs. They’re caused by small differences between environments that developers don’t think about until something breaks.</p><p>Here are eight problems I’ve encountered, the Python tools that helped me solve them, and the engineering habits I now use to avoid repeating the same mistakes.</p><h3>1. Environment Variables — The Configuration That Disappeared</h3><p>One of the first things I check when an application works locally but fails in production is its environment variables.</p><p>During development, it’s easy to get comfortable with a .env file.</p><p>You store your database URL, API keys, secret keys, and application settings there, and everything works.</p><p>Then you deploy.</p><p>Suddenly, the application complains that a configuration value doesn’t exist.</p><p>Why?</p><p>Because your local .env file doesn&#39;t automatically travel with your application.</p><p>Production environments need their own configuration.</p><p>I now use pydantic-settings to manage application settings more reliably.</p><p>It allows me to define configuration using Python types and validate values when the application starts.</p><p>For example:</p><pre>from pydantic_settings import BaseSettings, SettingsConfigDict<br><br>class Settings(BaseSettings):<br>    database_url: str<br>    secret_key: str<br>    debug: bool = False<br>    model_config = SettingsConfigDict(<br>        env_file=&quot;.env&quot;,<br>        env_file_encoding=&quot;utf-8&quot;,<br>        extra=&quot;ignore&quot;<br>    )<br><br>settings = Settings()Now, instead of accessing environment variables throughout the application, I have one centralized configuration object.</pre><pre>print(settings.database_url)</pre><p>The advantage isn’t just cleaner code.</p><p>If a required setting is missing, the application can fail immediately with a meaningful validation error rather than crashing somewhere deep inside an API request.</p><p>A small improvement, but one that can save hours of debugging.</p><p><strong>Libraries:</strong> pydantic-settings, python-dotenv</p><p><strong>Lesson:</strong> Never assume production has the same configuration as your development machine.</p><h3>2. Dependency Conflicts — The Package That Worked Yesterday</h3><p>Here’s another situation that took me a while to appreciate.</p><p>You install a Python package, write your code, test everything, and move on.</p><p>A few weeks later, you deploy the application on another machine.</p><p>Suddenly, something breaks.</p><p>The code hasn’t changed.</p><p>The package has.</p><p>Or perhaps another dependency installed a different version of a library that your application relies on.</p><p>Python’s package ecosystem is incredibly useful, but dependency management can become surprisingly complicated.</p><p>For example, imagine your application depends on:</p><ul><li>FastAPI</li><li>Pydantic</li><li>SQLAlchemy</li><li>OpenAI</li><li>Pandas</li></ul><p>Each package may have its own dependencies and version requirements.</p><p>Updating one library can introduce incompatibilities elsewhere.</p><p>This is why I never rely on whatever happens to be installed on my development machine.</p><p>I prefer explicitly managing dependencies and locking versions.</p><p>For smaller projects, a pinned requirements file can be enough:</p><pre>fastapi==0.120.0<br>pydantic==2.12.0<br>sqlalchemy==2.0.44<br>pandas==2.3.3</pre><p>These are illustrative version pins; use versions compatible with your project.</p><p>For larger applications, I also like using tools such as uv to manage dependencies and generate a lockfile.</p><p>The important distinction is that a requirements file with version ranges and a lockfile don’t necessarily provide the same reproducibility guarantees.</p><p>A lockfile captures a resolved dependency set, making it easier to recreate the same environment.</p><p><strong>Pro Tip:</strong> If an application works on your laptop but fails in CI, compare the installed dependency versions before rewriting the application code.</p><p>Sometimes the bug isn’t in your code.</p><p>It’s in the environment running it.</p><p><strong>Libraries and tools:</strong> uv, pip, pip-tools</p><h3>3. Database Connections — Why Local Databases Lie</h3><p>Local database configurations are often much more forgiving than production configurations.</p><p>During development, I might run PostgreSQL inside Docker or use a database installed directly on my machine.</p><p>The application connects successfully, queries return quickly, and everything looks fine.</p><p>Then production introduces a different reality.</p><p>The database might be hosted on another server, require SSL, enforce connection limits, or sit behind a network firewall.</p><p>Suddenly, database connection errors start appearing.</p><p>One issue I pay particular attention to is connection pooling.</p><p>Imagine an API receiving hundreds of requests.</p><p>If every request creates a completely new database connection, the application can quickly overwhelm the database.</p><p>SQLAlchemy provides connection pooling to reuse database connections more efficiently.</p><p>Here’s a basic example:</p><pre>from sqlalchemy import create_engine<br>DATABASE_URL = &quot;postgresql+psycopg://user:password@localhost/app&quot;<br>engine = create_engine(<br>    DATABASE_URL,<br>    pool_size=10,<br>    max_overflow=20,<br>    pool_pre_ping=True<br>)A few details matter here.</pre><ul><li>pool_size controls the number of persistent connections in the pool.</li><li>max_overflow allows additional temporary connections when demand increases.</li><li>pool_pre_ping checks whether a connection is still alive before using it.</li></ul><p>That last option is particularly useful when database connections might have been closed by an external server.</p><p>However, there’s something developers often overlook.</p><p>If you run five application workers, each with its own connection pool, your potential database connections multiply.</p><p>A pool size of 10 doesn’t necessarily mean your entire application uses only 10 connections.</p><p>It could mean 50 across five workers, before accounting for overflow.</p><p>That’s why connection pool settings should be designed around the database’s actual connection limits.</p><p><strong>Libraries:</strong> SQLAlchemy, psycopg</p><h3>4. File Paths — The Folder That Suddenly Doesn’t Exist</h3><p>This one looks almost too simple to cause production problems.</p><p>But it does.</p><p>Imagine writing a Python script that generates PDF reports.</p><p>Locally, you save them inside:</p><pre>output_path = &quot;reports/report.pdf&quot;</pre><p>The directory exists on your machine.</p><p>Everything works.</p><p>After deployment, the application throws:</p><pre>FileNotFoundError</pre><p>Why?</p><p>Because the working directory might be different, the folder might not exist, or the deployment environment might have a different filesystem structure.</p><p>I learned to stop treating relative paths as predictable.</p><p>Python’s pathlib makes file handling much cleaner:</p><pre>from pathlib import Path<br>BASE_DIR = Path(__file__).resolve().parent<br>OUTPUT_DIR = BASE_DIR / &quot;reports&quot;<br>OUTPUT_DIR.mkdir(<br>    parents=True,<br>    exist_ok=True<br>)<br>output_path = OUTPUT_DIR / &quot;report.pdf&quot;</pre><p>Now the application builds paths relative to the Python file rather than depending entirely on whichever directory happens to be active.</p><p>But there’s another production consideration.</p><p>Some hosting platforms use ephemeral filesystems.</p><p>That means files saved locally inside the application container might disappear when the application restarts or gets redeployed.</p><p>For user uploads, generated reports, and important application files, I prefer external object storage such as Amazon S3 or another compatible storage service.</p><p><strong>Lesson:</strong> A file existing on your laptop doesn’t mean it exists in production.</p><h3>5. CORS — The API Works, but the Browser Disagrees</h3><p>I used to find CORS errors particularly frustrating.</p><p>The backend was running.</p><p>The API endpoint responded correctly when tested through Postman.</p><p>But the frontend refused to access the response.</p><p>At first, I suspected the API itself.</p><p>It wasn’t.</p><p>The problem was Cross-Origin Resource Sharing (CORS).</p><p>Browsers enforce security restrictions when a frontend application tries to communicate with an API hosted on a different origin.</p><p>For example:</p><pre>Frontend: https://mywebsite.com<br>Backend:  https://api.mywebsite.com</pre><p>These are different origins.</p><p>Even though both belong to the same domain family, the browser treats them as separate origins.</p><p>FastAPI makes CORS configuration straightforward:</p><pre>from fastapi import FastAPI<br>from fastapi.middleware.cors import CORSMiddleware<br>app = FastAPI()<br>app.add_middleware(<br>    CORSMiddleware,<br>    allow_origins=[<br>        &quot;https://mywebsite.com&quot;<br>    ],<br>    allow_credentials=True,<br>    allow_methods=[&quot;GET&quot;, &quot;POST&quot;, &quot;PUT&quot;, &quot;DELETE&quot;],<br>    allow_headers=[&quot;Authorization&quot;, &quot;Content-Type&quot;],<br>)</pre><p>One important detail: avoid using allow_origins=[&quot;*&quot;] in production when credentials are involved.</p><p>Explicitly allow the origins your frontend actually uses.</p><p>Also, CORS isn’t an authentication mechanism.</p><p>It controls browser access to cross-origin responses. Your API still needs proper authentication and authorization.</p><p><strong>Libraries:</strong> FastAPI</p><h3>6. Logging — Stop Debugging Production With Print Statements</h3><p>There’s a difference between debugging an application locally and debugging one running on a production server.</p><p>Locally, I can open the terminal, reproduce an error, and inspect what’s happening.</p><p>In production, things aren’t always that convenient.</p><p>A user reports that a request failed.</p><p>The application logs show a generic server error.</p><p>And now I’m trying to figure out what happened without being able to reproduce it.</p><p>This is where structured logging becomes incredibly valuable.</p><p>Python already includes a logging module, so there’s no reason to depend entirely on print() statements.</p><p>Here’s a simple setup:</p><pre>import logging<br>logging.basicConfig(<br>    level=logging.INFO,<br>    format=&quot;%(asctime)s %(levelname)s %(name)s %(message)s&quot;<br>)<br>logger = logging.getLogger(__name__)<br><br>def process_payment(payment_id):<br>    logger.info(<br>        &quot;Processing payment: %s&quot;,<br>        payment_id<br>    )<br>    try:<br>        # Payment processing logic<br>        pass<br>    except Exception:<br>        logger.exception(<br>            &quot;Payment processing failed&quot;<br>        )<br>        raiseNotice the use of logger.exception().</pre><p>When called inside an exception handler, it records the exception traceback automatically.</p><p>For larger applications, I also consider structured logging with libraries such as structlog.</p><p>This makes it easier to attach useful context to log events:</p><ul><li>Request IDs</li><li>User IDs</li><li>Processing times</li><li>Task IDs</li><li>Error details</li></ul><p>One particularly useful practice is assigning a unique request ID to each incoming API request.</p><p>When something goes wrong, you can trace the request through multiple services instead of searching thousands of unrelated log entries.</p><p><strong>Pro Tip:</strong> Never log passwords, API keys, authentication tokens, or sensitive personal information. Production logs should help you debug problems without becoming a security problem themselves.</p><p><strong>Libraries:</strong> logging, structlog</p><h3>7. Docker — Finally, a Consistent Environment</h3><p>At some point, I got tired of hearing:</p><p>“But it works on my machine.”</p><p>It became one of those sentences that usually meant another debugging session was coming.</p><p>The problem was environmental inconsistency.</p><p>My laptop had one Python version.</p><p>The production server had another.</p><p>One machine had system dependencies installed.</p><p>The other didn’t.</p><p>Docker helped eliminate many of these differences.</p><p>Instead of manually configuring every machine, I could package the application and its runtime environment into a container.</p><p>Here’s a basic Dockerfile for a FastAPI application:</p><pre>FROM python:3.12-slim<br>WORKDIR /app<br>COPY requirements.txt .<br>RUN pip install --no-cache-dir -r requirements.txt<br>COPY . .<br>EXPOSE 8000<br>CMD [&quot;uvicorn&quot;, &quot;main:app&quot;, &quot;--host&quot;, &quot;0.0.0.0&quot;, &quot;--port&quot;, &quot;8000&quot;]Now the application can run inside a container with a predictable Python runtime and installed dependencies.</pre><p>But Docker isn’t magic.</p><p>A container doesn’t automatically guarantee identical production behavior.</p><p>You still need to manage environment variables, persistent storage, networking, secrets, and external services correctly.</p><p>For production, I also recommend:</p><ul><li>Running as a non-root user.</li><li>Using a .dockerignore file.</li><li>Pinning your base image appropriately.</li><li>Keeping secrets outside the image.</li><li>Adding health checks where appropriate.</li><li>Using multi-stage builds when they meaningfully reduce image size.</li></ul><p>One more thing: don’t copy your entire development environment into the production image.</p><p>A smaller, carefully constructed image is generally easier to maintain and deploy.</p><p><strong>Tools:</strong> Docker, Docker Compose</p><h3>8. Production Testing — Passing Tests Doesn’t Mean You’re Ready</h3><p>This might be the most important lesson of all.</p><p>For a long time, I treated successful local tests as a strong indication that deployment would go smoothly.</p><p>But tests only verify the behavior you’ve actually tested.</p><p>They don’t automatically check whether:</p><ul><li>Your production database is reachable.</li><li>Your environment variables are configured.</li><li>Your external API credentials are valid.</li><li>Your application can handle concurrent requests.</li><li>Your deployment has the correct permissions.</li><li>Your server can recover from unexpected failures.</li></ul><p>That’s why I’ve started treating production readiness as a separate engineering responsibility.</p><p>For Python applications, I commonly combine pytest with tools such as httpx for API testing.</p><p>For example:</p><pre>from fastapi.testclient import TestClient<br>from main import app<br>client = TestClient(app)<br><br>def test_health_endpoint():<br>    response = client.get(&quot;/health&quot;)<br>    assert response.status_code == 200<br>    assert response.json()[&quot;status&quot;] == &quot;ok&quot;This is a simple example, but the idea is important.</pre><p>I like having a health endpoint that verifies whether the application is running.</p><p>For more serious systems, I distinguish between:</p><p><strong>Liveness checks:</strong> Is the application process alive?</p><p><strong>Readiness checks:</strong> Is the application prepared to receive traffic?</p><p>An application might be alive but unable to connect to its database.</p><p>Treating those conditions as identical can cause unnecessary problems during deployments.</p><p>I also use CI pipelines to run tests before code reaches production.</p><p>A deployment should be the final step of a verification process, not the first time the application gets tested in an environment resembling reality.</p><p><strong>Libraries and tools:</strong> pytest, httpx, GitHub Actions</p><h3>The Deployment Checklist I Now Use</h3><p>After dealing with enough production failures, I stopped relying on memory to decide whether an application was ready.</p><p>I started using a simple checklist.</p><p>Before deploying a Python application, I check:</p><ul><li>Environment variables are configured correctly.</li><li>Dependencies are pinned and reproducible.</li><li>Database connectivity and connection limits are verified.</li><li>Required directories and file permissions exist.</li><li>CORS allows only the required frontend origins.</li><li>Logging captures useful diagnostic information.</li><li>Secrets are not included in source code or Docker images.</li><li>Automated tests pass in CI.</li><li>Health and readiness checks work.</li><li>Important files use persistent storage.</li><li>A rollback strategy exists.</li></ul><p>It doesn’t eliminate every production issue.</p><p>But it catches many of the mistakes that used to consume my afternoons.</p><h3>What I Finally Learned About Production</h3><p>The biggest mistake I made early in my development journey was thinking that deployment was simply another step after writing code.</p><p>It isn’t.</p><p>Production introduces a completely different set of responsibilities.</p><p>Your application now has to deal with real users, unpredictable traffic, external services, network failures, security requirements, and infrastructure that you don’t completely control.</p><p>A Python script that works perfectly on your laptop is only the beginning.</p><p>The real engineering challenge is making it reliable when the environment stops being predictable.</p><p>And that’s where tools such as Pydantic, SQLAlchemy, Docker, FastAPI, and pytest become much more than convenient libraries.</p><p>They help turn working code into dependable software.</p><p>One thing I’ve learned after years of building Python applications is this:</p><p><strong>Good developers write code that works. Better engineers build systems that keep working when things go wrong.</strong></p><p>And honestly, that’s the difference I wish I’d understood much earlier.</p><p>Now, whenever I finish a feature, I don’t just ask whether it works locally.</p><p>I ask a much more important question:</p><p><em>What could break when someone else runs this code?</em></p><p>That question has probably saved me more debugging time than any new Python library ever has.</p><h3>Thank you for being a part of the community</h3><p><em>Before you go:</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*d9QTaaaxboQP_gKSLedW_w.png" /></figure><p>👉 Be sure to <strong>clap</strong> and <strong>follow</strong> the writer ️👏<strong>️️</strong></p><p>👉 Follow us: <a href="https://medium.com/codetodeploy"><strong>Medium</strong></a></p><p><strong>Disclosure:</strong> This post includes affiliate and partnership links.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0315a611f62b" width="1" height="1" alt=""><hr><p><a href="https://medium.com/codetodeploy/my-website-worked-perfectly-locally-then-everything-broke-in-production-0315a611f62b">My Website Worked Perfectly Locally — Then Everything Broke in Production</a> was originally published in <a href="https://medium.com/codetodeploy">CodeToDeploy</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The Performance Problems I Didn’t Notice Until My Websites Got Bigger]]></title>
            <link>https://medium.com/codetodeploy/the-performance-problems-i-didnt-notice-until-my-websites-got-bigger-e26f13361aa2?source=rss-df9e89e9c50c------2</link>
            <guid isPermaLink="false">https://medium.com/p/e26f13361aa2</guid>
            <category><![CDATA[technology]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[web-development]]></category>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[python]]></category>
            <dc:creator><![CDATA[Lubnaaly]]></dc:creator>
            <pubDate>Thu, 29 Oct 2026 09:32:34 GMT</pubDate>
            <atom:updated>2026-10-29T18:53:26.852Z</atom:updated>
            <content:encoded><![CDATA[<h4><em>The hidden bottlenecks in Python, APIs, databases, and web applications that only become obvious when real users start showing up.</em></h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*NEz_6IJiOjqmofbH" /><figcaption>Photo by <a href="https://unsplash.com/@mparzuchowski?utm_source=medium&amp;utm_medium=referral">Michał Parzuchowski</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>When I first started building web applications, performance wasn’t something I worried about very much.</p><blockquote><em>💥 </em><strong><em>Master AI &amp; Tech Skills in 3 Months!</em></strong><br>💰 One Subscription. Multiple Courses. More Savings.<br>🔥 Get Up to 50% OFF | ⏰ Limited-Time Offer<br><a href="https://trk.udemy.com/zz4NBO"><em>👉 </em><strong><em>Enroll Now &amp; Start Learning</em></strong></a></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ITogvtC6mF2BUF35Qsd9pg.png" /></figure><p>My pages loaded quickly.</p><p>My API returned responses almost instantly.</p><p>My database queries worked perfectly.</p><p>Everything looked fine.</p><p>Then the projects grew.</p><p>The database accumulated more records. More users started hitting the same endpoints. Background tasks began competing with requests. Suddenly, operations that once took milliseconds started taking seconds.</p><p>The frustrating part?</p><p><strong>Most of my code hadn’t changed. The amount of work it had to handle had.</strong></p><p>That experience changed how I think about performance.</p><p>I used to assume that slow applications were caused by complicated algorithms or poorly written Python. Sometimes they are. But in real web applications, the biggest bottlenecks often hide in places that look completely harmless.</p><p>Here are the performance problems I learned to pay attention to after my applications started getting bigger.</p><h3>1. My Database Queries Were Doing Far More Work Than I Realized</h3><p>This was one of the first problems that changed how I approached backend development.</p><p>Imagine building an API that returns a list of blog posts.</p><p>A simple implementation might look something like this:</p><pre>@app.get(&quot;/posts&quot;)<br>def get_posts():<br>    posts = db.query(Post).all()<br>return [<br>        {<br>            &quot;title&quot;: post.title,<br>            &quot;author&quot;: post.author.name<br>        }<br>        for post in posts<br>    ]It looks reasonable.</pre><p>Fetch the posts, retrieve each author’s name, and return the results.</p><p>But there’s a potential problem hiding in that loop.</p><p>If the ORM lazily loads each author’s information, accessing post.author.name can trigger another database query for every post.</p><p>This is known as the <strong>N+1 query problem</strong>.</p><p>Instead of making one query to fetch posts, the application might execute one query for the posts and another query for each post’s author.</p><p>For 10 posts, that’s potentially 11 queries.</p><p>For 1,000 posts, it could become 1,001.</p><p>And that’s before considering other relationships or additional queries elsewhere in the request.</p><p>The application doesn’t necessarily become slow when you’re testing it with a handful of records. The problem becomes much more obvious as the dataset grows.</p><h3>What I do differently now</h3><p>I pay attention to the number of queries an endpoint executes, not just the amount of Python code it contains.</p><p>With SQLAlchemy, eager loading can help:</p><pre>from sqlalchemy import select<br>from sqlalchemy.orm import selectinload<br>stmt = (<br>    select(Post)<br>    .options(selectinload(Post.author))<br>)<br>posts = db.scalars(stmt).all()Here, selectinload() fetches related authors in a separate query for the collection of posts rather than issuing a query for each author.</pre><p>The exact loading strategy depends on the relationships and query patterns, but the principle is the same: fetch the data you need without making the database do unnecessary work.</p><p>I also inspect generated SQL, check query plans, and look for missing indexes when queries become expensive.</p><p>One important detail: adding indexes to every column isn’t the answer either. Indexes consume storage and add overhead to writes. They should support actual query patterns.</p><p><strong>The lesson:</strong> Before optimizing Python code, find out how much work your database is doing on its behalf.</p><p>Sometimes the slowest part of a Python application isn’t Python at all.</p><h3>2. My API Was Returning More Data Than Anyone Needed</h3><p>Early in development, returning an entire dataset feels convenient.</p><p>You query the database, serialize the results, and send everything to the client.</p><p>What’s the problem?</p><p>Suppose your application has 50,000 products. A frontend requests the product list, and the API returns every product with its description, metadata, category, and other related fields.</p><p>Even if the database query is reasonably fast, the application still has to:</p><ul><li>Retrieve the records.</li><li>Convert them into Python objects.</li><li>Serialize the response into JSON.</li><li>Transfer the response over the network.</li><li>Parse the JSON in the browser.</li></ul><p>Every step consumes resources.</p><p>And if multiple users make the same request simultaneously, the wasted work multiplies.</p><p>I learned that a query can be fast while the endpoint itself is painfully slow.</p><h3>Pagination makes a real difference</h3><p>Instead of returning every record, return a limited number at a time.</p><pre>from fastapi import FastAPI, Query<br>from sqlalchemy import select<br>app = FastAPI()<br>@app.get(&quot;/products&quot;)<br>def get_products(<br>    limit: int = Query(default=50, ge=1, le=100),<br>    offset: int = Query(default=0, ge=0),<br>):<br>    stmt = (<br>        select(Product)<br>        .order_by(Product.id)<br>        .limit(limit)<br>        .offset(offset)<br>    )<br>    return db.scalars(stmt).all()Now the client can request a manageable page of results instead of downloading the entire table.</pre><p>For larger datasets, cursor-based pagination can be more efficient than large offsets, especially when the query is supported by an appropriate index.</p><p>I also try to return only the fields the client actually needs. A product listing usually doesn’t need the full product description, internal metadata, and every associated record.</p><p>There’s no reason to send 20 fields when five will do.</p><p><strong>The lesson:</strong> Performance isn’t only about how quickly your server calculates an answer. It’s also about how much unnecessary work you ask it to do.</p><h3>3. I Thought Async Python Would Automatically Make Everything Faster</h3><p>When I started exploring asynchronous Python, the appeal was obvious.</p><p>FastAPI supports asynchronous endpoints. Python has asyncio. Network requests can overlap instead of waiting for each other.</p><p>It sounds like a straightforward performance upgrade.</p><p>But there’s a catch.</p><p><strong>async doesn&#39;t magically make blocking code asynchronous.</strong></p><p>Consider this example:</p><pre>import time<br>from fastapi import FastAPI<br>app = FastAPI()<br>@app.get(&quot;/report&quot;)<br>async def generate_report():<br>    time.sleep(3)<br>    return {&quot;status&quot;: &quot;complete&quot;}The endpoint is declared with async def, but time.sleep(3) blocks the event loop.</pre><p>While that blocking operation is running, other asynchronous requests sharing that event loop may be unable to make progress.</p><p>The function is asynchronous in syntax, not in behavior.</p><p>For a simple demonstration, replacing the sleep with await asyncio.sleep(3) allows other coroutines to run during the wait. In a real application, the same principle applies to network clients, database drivers, and other I/O operations.</p><pre>import asyncio<br>@app.get(&quot;/report&quot;)<br>async def generate_report():<br>    await asyncio.sleep(3)<br>    return {&quot;status&quot;: &quot;complete&quot;}</pre><p>Of course, real report generation shouldn’t be replaced with a sleep. The example simply illustrates the difference between blocking and non-blocking waits.</p><p>When working with external APIs, I use asynchronous clients when the surrounding application benefits from concurrency. For CPU-heavy work, I consider a process pool or a dedicated worker instead.</p><p>There’s another detail worth remembering: asynchronous code can improve throughput, but it doesn’t automatically reduce the time required for an individual operation.</p><p>An API request that takes two seconds to complete may still take two seconds after you make it asynchronous.</p><p>The gain comes from handling other work while waiting.</p><p><strong>The lesson:</strong> Choose concurrency based on the bottleneck you’re solving. Don’t add async everywhere and expect a performance miracle.</p><h3>4. My Application Kept Repeating Work It Had Already Done</h3><p>One of the easiest ways to waste computing resources is to repeatedly calculate or retrieve information that rarely changes.</p><p>Imagine an API that returns a dashboard containing:</p><ul><li>Total registered users.</li><li>Monthly revenue.</li><li>Number of active subscriptions.</li><li>Recent activity.</li><li>Popular products.</li></ul><p>Every time someone opens the dashboard, the backend runs the same aggregation queries.</p><p>If the dashboard receives frequent requests and the underlying data changes infrequently, much of that work may be unnecessary.</p><p>This is where caching becomes useful.</p><h3>A simple Redis example</h3><p>Redis can store frequently requested results so that the application doesn’t need to recalculate them on every request.</p><pre>import json<br>from redis import Redis<br>redis_client = Redis(<br>    host=&quot;localhost&quot;,<br>    port=6379,<br>    decode_responses=True,<br>)<br>def get_dashboard():<br>    cached = redis_client.get(&quot;dashboard:v1&quot;)<br>    if cached is not None:<br>        return json.loads(cached)<br>    dashboard = calculate_dashboard()<br>    redis_client.setex(<br>        &quot;dashboard:v1&quot;,<br>        60,<br>        json.dumps(dashboard),<br>    )<br>    return dashboardThis example caches the dashboard for 60 seconds.</pre><p>If another request arrives during that period, the application can return the cached result instead of repeating the aggregation.</p><p>But caching introduces its own engineering problems.</p><p>What happens when the data changes? What if two requests attempt to rebuild an expensive cache simultaneously? What if the cached result contains user-specific information?</p><p>A time-to-live helps limit how long old results remain available, but it doesn’t guarantee that every response is fresh. More demanding applications may need explicit invalidation, versioned cache keys, or protections against cache stampedes.</p><p>And I wouldn’t cache every endpoint just because Redis is available.</p><p>For frequently requested, expensive-to-compute data, caching can be valuable. For inexpensive queries against frequently changing data, it may add complexity without providing much benefit.</p><p><strong>The lesson:</strong> The fastest computation is often the one your application doesn’t need to repeat.</p><h3>5. My Background Tasks Were Making My API Feel Slow</h3><p>At some point, an application stops doing only simple database operations.</p><p>It might generate PDF reports, process uploaded documents, extract text from hundreds of files, call several AI APIs, or generate embeddings for a collection of documents.</p><p>These tasks can take seconds or minutes.</p><p>The first implementation is often straightforward:</p><pre>@app.post(&quot;/process-document&quot;)<br>async def process_document():<br>    result = process_large_document()<br>    return {&quot;result&quot;: result}</pre><p>There’s nothing inherently wrong with a synchronous workflow for a small task. The problem appears when expensive processing keeps the request open or blocks the execution environment.</p><p>Users end up waiting for work that doesn’t necessarily need to happen before the API responds.</p><p>For CPU-heavy operations, declaring the endpoint async doesn&#39;t solve the problem either.</p><h3>Move expensive work out of the request</h3><p>A more scalable design separates accepting a job from executing it.</p><p>The API receives the request, creates a job record, and sends the work to a background worker. The client receives a job ID and can check its status later.</p><p>Tools such as Celery can help manage distributed background tasks, retries, and queues.</p><p>A simplified workflow looks like this:</p><pre>@app.post(&quot;/process-document&quot;)<br>def submit_document():<br>    job = create_job_record()<br>process_document_task.delay(job.id)<br>    return {<br>        &quot;job_id&quot;: job.id,<br>        &quot;status&quot;: &quot;queued&quot;,<br>    }The worker performs the expensive processing separately.</pre><p>The client can then poll a status endpoint or receive updates through another mechanism.</p><p>This approach makes the API more responsive, but it doesn’t make the underlying job disappear. You still need to consider queue length, worker capacity, retry behavior, duplicate execution, and failed jobs.</p><p>For example, if 10,000 documents arrive but your workers can process only 100 per minute, the queue will grow. Monitoring queue age and throughput becomes just as important as monitoring API latency.</p><p>I also avoid sending large files directly through a queue when storing them in object storage and passing a reference would be more appropriate.</p><p><strong>The lesson:</strong> Your API should coordinate work, not necessarily perform every expensive operation before responding.</p><h3>6. I Was Guessing Where My Code Was Slow</h3><p>This was probably the most important change in my approach to optimization.</p><p>It’s surprisingly easy to spend an afternoon optimizing a function that has almost no effect on overall performance.</p><p>A loop looks suspicious, so you rewrite it.</p><p>A list comprehension seems inefficient, so you replace it.</p><p>You shave a few milliseconds off a calculation.</p><p>Meanwhile, a database query takes 800 milliseconds on every request.</p><p>I’ve learned to stop guessing.</p><p>Before optimizing, I try to answer three questions:</p><ol><li>Where is the time actually being spent?</li><li>How often does that operation run?</li><li>Does it materially affect the user experience or system capacity?</li></ol><h3>Start with the right tools</h3><p>Python includes a useful profiler:</p><pre>python -m cProfile -s cumulative app_script.py</pre><p>This helps identify functions consuming significant cumulative execution time in a script. For web applications, you may need to profile representative requests or workloads rather than simply running the application itself.</p><p>For more detailed analysis, tools such as py-spy can help inspect a running Python process with relatively low overhead.</p><p>I also measure individual requests.</p><p>For an API, I look at:</p><ul><li>Median response time.</li><li>p95 and p99 latency.</li><li>Request throughput.</li><li>Error rates and timeouts.</li><li>Database query duration.</li><li>CPU and memory utilization.</li></ul><p>Why p95?</p><p>Because an average can hide a terrible experience for a subset of users.</p><p>If most requests take 100 milliseconds but a small number take five seconds, the average might not reveal how frustrating the slow requests are.</p><p>The 95th-percentile latency tells you the response time below which 95% of measured requests completed.</p><p>I also test under realistic load. A website that responds quickly to one request may behave very differently when hundreds of requests arrive together.</p><blockquote><strong><em>Pro Tip:</em></strong><em> Never trust an optimization because the code looks faster. Measure the same workload before and after the change.</em></blockquote><p>The goal isn’t to produce clever code. It’s to improve a measurable outcome.</p><h3>7. My Memory Usage Became a Problem Before I Expected</h3><p>Not every performance problem appears as a slow response.</p><p>Sometimes the application works perfectly until memory consumption grows, garbage collection becomes more frequent, or the process gets killed by the operating system.</p><p>A common example is loading an entire file into memory when the application only needs to process it incrementally.</p><p>Consider a CSV file:</p><pre>import pandas as pd<br>df = pd.read_csv(&quot;large_dataset.csv&quot;)</pre><p>For a dataset that comfortably fits in memory, this is perfectly reasonable.</p><p>But as the file grows, the DataFrame may consume substantially more memory than the original CSV because data must be represented as in-memory objects and arrays.</p><p>If the application processes several files simultaneously, memory usage can grow quickly.</p><p>For suitable workloads, Pandas supports chunked reading:</p><pre>import pandas as pd<br>for chunk in pd.read_csv(<br>    &quot;large_dataset.csv&quot;,<br>    chunksize=10_000,<br>):<br>    process_chunk(chunk)Instead of loading the entire dataset at once, the application processes smaller portions.</pre><p>This works particularly well when each chunk can be handled independently or when intermediate results can be accumulated efficiently.</p><p>Of course, chunking isn’t always the answer. Operations that require comparing every record with every other record may need a different strategy. Sometimes a database, a streaming approach, or a more appropriate data-processing engine is a better fit.</p><p>I also pay attention to unnecessary copies of large DataFrames, unbounded in-memory caches, and lists that keep growing because old entries are never discarded.</p><p><strong>The lesson:</strong> Design data processing around the amount of information you can afford to keep in memory, not just the size of the input file.</p><h3>8. I Learned That Faster Code Doesn’t Always Mean a Faster Website</h3><p>Here’s something that took me a while to appreciate.</p><p>An application can have efficient Python functions and still deliver a poor user experience.</p><p>Why?</p><p>Because overall response time depends on everything involved in serving the request.</p><p>A simplified request might spend its time like this:</p><p>OperationTimeDatabase query120 msExternal API call700 msPython processing30 msResponse serialization20 msTotal870 ms</p><p>These numbers are illustrative, not benchmark results.</p><p>In this example, optimizing the Python processing from 30 milliseconds to 10 milliseconds saves only 20 milliseconds. That’s a modest improvement to a request that takes 870 milliseconds.</p><p>Reducing the external API call’s latency, running independent requests concurrently, or caching an appropriate result could have a much greater impact.</p><p>This is why I like breaking request latency into separate stages.</p><p>Without that breakdown, it’s easy to optimize the most interesting part of the system instead of the most expensive one.</p><p>I also distinguish between latency and throughput.</p><p>Latency tells me how long an operation takes. Throughput tells me how much work the system completes in a given period.</p><p>An optimization that reduces individual request latency may not improve throughput if the database remains the limiting factor. Likewise, a system may handle more concurrent requests without making every request individually faster.</p><p>Both measurements matter, depending on the problem.</p><p><strong>The lesson:</strong> Optimize the entire request path, not just the Python function you’re currently looking at.</p><h3>The Small Habit That Changed How I Build Web Applications</h3><p>I used to think about performance after a feature was finished.</p><p>First, I’d make it work.</p><p>Then I’d add more functionality.</p><p>If it eventually became slow, I’d investigate.</p><p>That approach wasn’t always disastrous, but it made some problems harder to diagnose because I hadn’t measured how the system behaved before it grew.</p><p>Now I try to build a few simple habits into my development process.</p><ul><li><strong>Measure early.</strong> Establish a baseline before making major performance changes.</li><li><strong>Watch database queries.</strong> Pay attention to query count, execution time, indexes, and query plans.</li><li><strong>Limit unnecessary work.</strong> Use pagination, select only required fields, and avoid repeated calculations.</li><li><strong>Choose the right execution model.</strong> Use asynchronous I/O for suitable I/O-bound work and separate workers for expensive tasks.</li><li><strong>Test realistic workloads.</strong> Include larger datasets, concurrent requests, and slow external dependencies.</li><li><strong>Recheck every optimization.</strong> A change that helps one workload may hurt another.</li></ul><p>I don’t try to make every function as fast as possible.</p><p>That would be a great way to spend weeks solving problems nobody has.</p><p>Instead, I focus on the parts of the application that affect users, consume significant resources, or limit how much work the system can handle.</p><h3>Final Thoughts</h3><p>The biggest performance problems I encountered weren’t always the most complicated ones.</p><p>They were the ones I didn’t notice.</p><p>A database query that ran a little too often. An API returning too much data. A blocking function inside an asynchronous endpoint. A background task that should never have been part of the request. A large dataset that no longer fit comfortably in memory.</p><p>Each problem looked manageable when the application was small.</p><p>Growth simply made the cost impossible to ignore.</p><p>That’s the part of backend development I find most interesting: scaling an application isn’t always about introducing more sophisticated technology. Sometimes it’s about understanding the work your existing code performs and removing what doesn’t need to happen.</p><p>You don’t always need another framework, a new database, or a complete rewrite.</p><p>Sometimes you need a query counter, a profiler, and an honest look at your request logs.</p><p><strong>Build for correctness first. Measure performance before guessing. And when something becomes slow, find the bottleneck before reaching for a solution.</strong></p><p>Because the code that works beautifully with 100 records isn’t necessarily the code that will behave beautifully with a million.</p><p>And learning to recognize the difference is one of the most valuable skills a Python developer can develop.</p><h3>Thank you for being a part of the community</h3><p><em>Before you go:</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*d9QTaaaxboQP_gKSLedW_w.png" /></figure><p>👉 Be sure to <strong>clap</strong> and <strong>follow</strong> the writer ️👏<strong>️️</strong></p><p>👉 Follow us: <a href="https://medium.com/codetodeploy"><strong>Medium</strong></a></p><p><strong>Disclosure:</strong> This post includes affiliate and partnership links.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e26f13361aa2" width="1" height="1" alt=""><hr><p><a href="https://medium.com/codetodeploy/the-performance-problems-i-didnt-notice-until-my-websites-got-bigger-e26f13361aa2">The Performance Problems I Didn’t Notice Until My Websites Got Bigger</a> was originally published in <a href="https://medium.com/codetodeploy">CodeToDeploy</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The CSS Techniques I Ignored as a Beginner but Use in Almost Every Project]]></title>
            <link>https://medium.com/codetodeploy/the-css-techniques-i-ignored-as-a-beginner-but-use-in-almost-every-project-39a159f1142d?source=rss-df9e89e9c50c------2</link>
            <guid isPermaLink="false">https://medium.com/p/39a159f1142d</guid>
            <category><![CDATA[web-development]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[data-science]]></category>
            <dc:creator><![CDATA[Lubnaaly]]></dc:creator>
            <pubDate>Mon, 26 Oct 2026 09:52:11 GMT</pubDate>
            <atom:updated>2026-10-30T11:18:26.992Z</atom:updated>
            <content:encoded><![CDATA[<h4>I used to focus on making things look right. Over time, I learned that writing maintainable CSS matters just as much as getting the design right.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*q7RViK-BgziSNRpw" /><figcaption>Photo by <a href="https://unsplash.com/@ajaygorecha?utm_source=medium&amp;utm_medium=referral">Ajay Gorecha</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>read here.</p><p>When I first started writing CSS, I thought I was pretty good at it.</p><blockquote><em>💥 </em><strong><em>Master AI &amp; Tech Skills in 3 Months!</em></strong><br>💰 One Subscription. Multiple Courses. More Savings.<br>🔥 Get Up to 50% OFF | ⏰ Limited-Time Offer<br><a href="https://trk.udemy.com/zz4NBO"><em>👉 </em><strong><em>Enroll Now &amp; Start Learning</em></strong></a></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ITogvtC6mF2BUF35Qsd9pg.png" /></figure><p>I could center elements, change colors, build navigation bars, and make a page look reasonably close to a design.</p><p>Then I had to change that design.</p><p>A small adjustment to the layout broke three other sections. A heading looked perfect on my laptop but overflowed on mobile. And don’t even get me started on the mysterious margins that seemed to exist only to fix problems caused by other margins.</p><p>Sound familiar? 😅</p><p>For a while, I thought this was just part of frontend development.</p><p>It wasn’t.</p><p>The real problem was that I was learning how to make CSS work without learning how to make it maintainable.</p><p>After spending more time building websites, debugging layouts, and revisiting old projects, I started relying on techniques I had previously ignored.</p><p>Some of them looked too simple to matter. Others seemed unnecessarily complicated.</p><p>Today, they’re among the first things I reach for when writing CSS.</p><p>Here are eight techniques that genuinely changed how I approach frontend development.</p><h3>1. Flexbox and Grid: Stop Fighting Your Layout</h3><p>One of my earliest CSS habits was using margins to position everything.</p><p>Need an element farther to the right? Add margin-left.</p><p>Need some space above it? Add margin-top.</p><p>Need two elements aligned? Add a few more properties and hope for the best.</p><p>It worked until the content changed.</p><p>The moment a heading became longer or a container got narrower, the layout started falling apart.</p><p>That’s when I began taking Flexbox and Grid more seriously.</p><h3>Use Flexbox for one-dimensional layouts</h3><p>Flexbox works particularly well when you’re arranging elements in a row or column.</p><p>For example, consider a navigation bar with a logo on the left and navigation links on the right.</p><p>Instead of manually positioning both sides, I can write:</p><pre>.navbar {<br>  display: flex;<br>  align-items: center;<br>  justify-content: space-between;<br>  gap: 1rem;<br>}</pre><p>That’s it.</p><p>The browser handles the alignment and spacing between the two sides.</p><p>If the content changes, the layout can adapt without requiring me to recalculate margins.</p><h3>Use Grid when rows and columns matter</h3><p>Now imagine building a dashboard with several cards.</p><p>You could use Flexbox, but CSS Grid gives you a more natural way to define a two-dimensional layout.</p><pre>.dashboard {<br>  display: grid;<br>  grid-template-columns: repeat(3, minmax(0, 1fr));<br>  gap: 1.5rem;<br>}</pre><p>Each card occupies one column, and gap controls the spacing.</p><p>The interesting part is minmax(0, 1fr).</p><p>The zero minimum allows a grid track to shrink below its content’s intrinsic minimum size. This helps prevent long text or wide content from forcing a column wider than intended.</p><p>It’s one of those tiny details you might ignore when learning Grid but appreciate when debugging a real dashboard.</p><p><strong>My rule of thumb:</strong> Use Flexbox when the arrangement primarily follows one direction. Use Grid when you need to manage rows and columns together.</p><p>Neither tool is inherently better. The trick is choosing the one that matches the problem.</p><h3>2. CSS Custom Properties: Stop Repeating Yourself</h3><p>I once worked on a layout where the same shade of blue appeared in buttons, links, headings, and several background elements.</p><p>Initially, that seemed harmless.</p><p>Then the design changed.</p><p>What should have been a simple color adjustment turned into a search through multiple stylesheets.</p><p>I had duplicated the same value everywhere, so every change required more work than it should have.</p><p>CSS custom properties solve this problem.</p><pre>:root {<br>  --color-primary: #2563eb;<br>  --color-text: #1f2937;<br>  --color-muted: #6b7280;<br>  --spacing-md: 1rem;<br>  --radius-md: 0.75rem;<br>}<br>.button {<br>  background-color: var(--color-primary);<br>  border-radius: var(--radius-md);<br>  padding: var(--spacing-md);<br>}<br>.heading {<br>  color: var(--color-text);<br>}<br>.description {<br>  color: var(--color-muted);<br>}Now the design values live in one place.</pre><p>If the primary color changes, I update one variable instead of hunting through the entire codebase.</p><p>But there’s a more important benefit.</p><p>Custom properties make design decisions explicit.</p><p>When I see var(--spacing-md), I understand that the value belongs to a spacing system. When I see 17px repeated throughout a stylesheet, I have to guess whether those values were intentional or simply accumulated over time.</p><p>For larger projects, this becomes especially useful when implementing themes.</p><pre>:root {<br>  --surface: #ffffff;<br>  --text: #111827;<br>}<br>[data-theme=&quot;dark&quot;] {<br>  --surface: #111827;<br>  --text: #f9fafb;<br>}<br>body {<br>  background: var(--surface);<br>  color: var(--text);<br>}Changing the data-theme attribute lets the same components use different colors without rewriting their individual styles.</pre><p>One important distinction: CSS custom properties aren’t just preprocessor variables. They participate in the CSS cascade and can change at runtime.</p><p>That makes them useful for theming, component customization, and values controlled by JavaScript.</p><p><strong>The lesson:</strong> If a value represents a reusable design decision, give it a name.</p><p>Your future self will thank you.</p><h3>3. clamp(): Responsive Typography Without a Media Query for Everything</h3><p>Responsive design used to mean writing media queries for every little adjustment.</p><p>A heading looked too large on mobile? Add a breakpoint.</p><p>It looked too small on a tablet? Add another.</p><p>Then the design changed, and the carefully chosen values stopped making sense.</p><p>CSS’s clamp() function offers a simpler approach for many of these situations.</p><p>It accepts three values:</p><pre>clamp(minimum, preferred, maximum)</pre><p>The browser tries to use the preferred value but keeps it within the specified minimum and maximum.</p><p>Here’s how I use it for a responsive heading:</p><pre>.hero-title {<br>  font-size: clamp(2rem, 1.2rem + 3vw, 4rem);<br>  line-height: 1.1;<br>}</pre><p>The heading scales with the viewport instead of jumping abruptly between fixed font sizes.</p><p>On smaller screens, it stays above the minimum. On larger screens, it stops growing at the maximum.</p><p>And I don’t need a separate media query just to adjust its size.</p><p>The same technique works for spacing:</p><pre>.hero {<br>  padding-block: clamp(2rem, 6vw, 6rem);<br>  padding-inline: clamp(1rem, 4vw, 3rem);<br>}</pre><p>This creates fluid spacing while keeping the extremes under control.</p><p>There is one detail worth remembering: vw alone can produce awkward results because text might become too small or too large at the extremes. Combining viewport units with a relative base value, then setting sensible limits, usually produces more predictable behavior.</p><p>Also, clamp() isn&#39;t a replacement for every media query. Sometimes a layout needs a genuine structural change rather than gradual scaling.</p><p><strong>The takeaway:</strong> Let CSS handle continuous adjustments. Save media queries for changes that actually need a breakpoint.</p><h3>4. :is() and :where(): Write Selectors You Can Maintain</h3><p>As a stylesheet grows, selectors can become surprisingly complicated.</p><p>You might have several elements that share the same styling, but each one requires a different selector.</p><p>For example:</p><pre>.card h2,<br>.card h3,<br>.card h4 {<br>  margin-block: 0 0.75rem;<br>}</pre><p>There’s nothing wrong with this.</p><p>But imagine repeating the same pattern across a much larger stylesheet.</p><p>The :is() pseudo-class lets you group selector alternatives:</p><pre>.card :is(h2, h3, h4) {<br>  margin-block: 0 0.75rem;<br>}</pre><p>This becomes particularly useful when the selectors themselves are more complicated.</p><p>For example:</p><pre>:is(.article, .sidebar) :is(h2, h3) {<br>  line-height: 1.25;<br>}</pre><p>There’s also :where(), which looks similar but has a useful difference: its specificity is always zero.</p><p>Why does that matter?</p><p>Because specificity determines which competing CSS rule wins when multiple declarations apply to the same element.</p><p>Consider a reusable component stylesheet. If its default styles have unnecessarily high specificity, overriding them later can become frustrating.</p><p>Using :where() helps keep defaults easy to override:</p><pre>:where(.card) h2 {<br>  margin-top: 0;<br>}</pre><p>The selector contributes zero specificity for the entire :where() argument, making the rule easier to override with another appropriately scoped selector.</p><p>This is particularly useful when building reusable components, design systems, and base stylesheets.</p><p>One caution: :is() takes the specificity of its most specific argument, while :where() always contributes zero specificity. They look alike, but they aren&#39;t interchangeable in every situation.</p><p><strong>My advice:</strong> Don’t optimize selectors just to make them shorter. Use these pseudo-classes when they improve readability or make your styles easier to override.</p><h3>5. Mobile-First CSS: Start Small, Then Add Complexity</h3><p>I used to build desktop layouts first and then try to squeeze them onto smaller screens.</p><p>Sometimes that worked.</p><p>Other times, the mobile version became a collection of exceptions.</p><p>The sidebar needed to disappear. The columns needed to stack. The font sizes needed to shrink. The navigation needed an entirely different layout.</p><p>The problem wasn’t that media queries were bad.</p><p>I had simply started with the wrong assumption: that the desktop layout should be the default everywhere.</p><p>A mobile-first approach starts with the smaller layout and adds complexity as more space becomes available.</p><pre>.product-grid {<br>  display: grid;<br>  grid-template-columns: 1fr;<br>  gap: 1rem;<br>}<br>@media (min-width: 40rem) {<br>  .product-grid {<br>    grid-template-columns: repeat(2, minmax(0, 1fr));<br>  }<br>}<br>@media (min-width: 64rem) {<br>  .product-grid {<br>    grid-template-columns: repeat(3, minmax(0, 1fr));<br>  }<br>}The default layout uses one column.</pre><p>At wider viewport sizes, it expands to two and then three columns.</p><p>This is easy to understand because each breakpoint introduces a specific enhancement rather than undoing a collection of desktop assumptions.</p><p>But here’s something I learned later: <strong>mobile-first doesn’t mean designing exclusively for mobile.</strong></p><p>It means establishing a sensible baseline and progressively adapting the layout.</p><p>You should still test at intermediate widths. A page can look perfect at 390 pixels and 1440 pixels but fall apart somewhere between them.</p><p>And don’t choose breakpoints simply because a particular phone or tablet exists. Choose them when your content needs the layout to change.</p><p>That’s a much more durable approach.</p><h3>6. CSS Logical Properties: Make Layouts Less Dependent on Direction</h3><p>This technique took me longer to appreciate than it should have.</p><p>Most developers learn properties such as:</p><pre>margin-left: 1rem;<br>padding-right: 1rem;</pre><p>They’re perfectly valid.</p><p>But they describe physical directions rather than the logical structure of the content.</p><p>That distinction becomes important when supporting right-to-left languages or different writing modes.</p><p>Logical properties let you describe spacing in terms of the content’s inline and block directions.</p><p>For example:</p><pre>.card {<br>  padding-inline: 1.5rem;<br>  padding-block: 1rem;<br>}<br>.card-title {<br>  margin-block-end: 0.75rem;<br>}</pre><p>Here:</p><ul><li>padding-inline applies padding along the inline direction.</li><li>padding-block applies padding along the block direction.</li><li>margin-block-end adds space at the end of the block direction.</li></ul><p>For a typical left-to-right horizontal layout, the inline direction runs from left to right and the block direction runs from top to bottom.</p><p>In a right-to-left layout, the inline direction adapts accordingly.</p><p>That means your CSS can respond to the writing direction without requiring you to replace every left and right declaration manually.</p><p>For example, a component with an icon and text can use logical spacing:</p><pre>.icon {<br>  margin-inline-end: 0.5rem;<br>}</pre><p>The spacing stays on the end side of the inline content direction.</p><p>This is particularly helpful when building internationalized applications.</p><p>Even if you never plan to support right-to-left languages, logical properties can make your stylesheets more expressive because they describe relationships rather than fixed screen directions.</p><p><strong>The bigger lesson:</strong> Write CSS that describes what you mean, not just where something happens to appear today.</p><h3>7. Container Queries: Stop Making Every Component Care About the Viewport</h3><p>Here’s a problem that appears when a project grows.</p><p>You build a beautiful card for a full-width page.</p><p>Then someone places the same card inside a narrow sidebar.</p><p>The viewport hasn’t changed, so your media queries don’t activate. But the card has much less room, and its layout no longer works.</p><p>This is where container queries become useful.</p><p>Traditional media queries respond to conditions such as viewport width.</p><p>Container queries let a component respond to the size of a designated container.</p><p>First, define the container:</p><pre>.card-wrapper {<br>  container-type: inline-size;<br>}</pre><p>Then style the component based on the container’s available width:</p><pre>.product-card {<br>  display: grid;<br>  grid-template-columns: 1fr;<br>  gap: 1rem;<br>}<br>@container (min-width: 32rem) {<br>  .product-card {<br>    grid-template-columns: 10rem minmax(0, 1fr);<br>    align-items: center;<br>  }<br>}When the container reaches the specified width, the card changes from a single-column layout to a two-column layout.</pre><p>The same component can now work in a wide content area and a narrow sidebar without relying on the overall viewport size.</p><p>This is a big deal for reusable UI.</p><p>Think about:</p><ul><li>Product cards used across multiple pages.</li><li>Dashboard widgets.</li><li>Article previews.</li><li>Components inside resizable panels.</li><li>Reusable components in a design system.</li></ul><p>Instead of making every component aware of the page layout, you give it the ability to respond to the space it actually receives.</p><p>There is one important detail: a container query needs an appropriate containment context, such as the container-type: inline-size declaration above. Without it, the browser doesn&#39;t have the designated query container you&#39;re expecting.</p><p><strong>My takeaway:</strong> Media queries help you adapt the page. Container queries help you adapt the component.</p><p>That distinction makes reusable CSS much easier to reason about.</p><h3>8. min(), max(), and minmax(): Let the Browser Calculate the Layout</h3><p>I used to write fixed widths for containers and then add media queries when they stopped fitting.</p><p>For example, a content section might need a maximum width of 1200 pixels while still leaving space around it on smaller screens.</p><p>You could solve that with multiple declarations and breakpoints.</p><p>Or you could write:</p><pre>.container {<br>  width: min(100% - 2rem, 75rem);<br>  margin-inline: auto;<br>}</pre><p>The min() function selects the smaller of its arguments.</p><p>So the container uses the available width minus two rems when space is limited, but it never exceeds 75 rems.</p><p>The result is a centered layout with responsive side spacing and a maximum width.</p><p>No additional media query is necessary for that particular behavior.</p><p>The related max() function chooses the larger value:</p><pre>.section {<br>  padding-inline: max(1rem, 4vw);<br>}</pre><p>This keeps the padding from dropping below one rem while allowing it to grow on wider screens.</p><p>Then there’s minmax(), which is especially useful with CSS Grid.</p><pre>.gallery {<br>  display: grid;<br>  grid-template-columns: repeat(<br>    auto-fit,<br>    minmax(min(100%, 16rem), 1fr)<br>  );<br>  gap: 1rem;<br>}</pre><p>This creates a flexible gallery where columns can fit into the available space without becoming narrower than the usable minimum. The nested min() also helps the grid behave when the container itself is narrower than 16 rems.</p><p>As the available width changes, the browser determines how many columns can fit.</p><p>You don’t need to write a media query for every possible number of cards.</p><p>That’s the beauty of intrinsic and fluid layout techniques: you describe the constraints, and the browser calculates a suitable arrangement.</p><p>Of course, these functions aren’t magic. You still need to understand the available space, minimum sizes, and content constraints.</p><p>But once you get comfortable with them, you can eliminate a surprising amount of breakpoint-specific CSS.</p><p><strong>The principle:</strong> Describe the layout’s limits instead of hardcoding every possible outcome.</p><h3>The CSS Habit That Made the Biggest Difference</h3><p>Learning these techniques helped me solve specific problems.</p><p>But the biggest improvement came from changing how I approached CSS in the first place.</p><p>I stopped asking:</p><p><em>“How can I make this element look right? “</em></p><p>And started asking:</p><p><em>“What rules would keep this layout working when the content, screen size, or design changes?”</em></p><p>That question leads to different decisions.</p><p>Instead of adding arbitrary margins, I look at the layout system.</p><p>Instead of repeating colors, I define custom properties.</p><p>Instead of creating a media query for every font-size adjustment, I consider fluid sizing.</p><p>Instead of making reusable components depend on the viewport, I consider container queries.</p><p>None of these techniques is particularly difficult on its own.</p><p>The difference comes from knowing when to use each one.</p><p>Here’s a small checklist I use when reviewing CSS:</p><ul><li>Can Flexbox or Grid replace fragile positioning?</li><li>Are repeated design values represented by custom properties?</li><li>Can fluid sizing eliminate unnecessary breakpoints?</li><li>Are selectors easy to override?</li><li>Does the layout work at intermediate screen widths?</li><li>Would logical properties make the component more adaptable</li><li>Should this component respond to its container instead of the viewport?</li><li>Can intrinsic sizing replace hardcoded dimensions?</li></ul><p>You don’t need to apply every technique to every component. Overengineering a simple layout is still overengineering.</p><p>The goal is to choose the simplest approach that remains reliable as the project grows.</p><blockquote><strong><em>Pro Tip:</em></strong><em> Good CSS isn’t the CSS with the fewest lines. It’s the CSS that makes the next change predictable.</em></blockquote><h3>Final Thoughts</h3><p>I used to think getting better at CSS meant memorizing more properties.</p><p>Now I think it means understanding the browser’s layout system well enough to stop fighting it.</p><p>Flexbox and Grid handle relationships between elements. Custom properties keep design decisions consistent. clamp() makes sizing fluid. Logical properties make components more adaptable. Container queries help reusable components respond to their surroundings.</p><p>Each technique solves a different problem, but they share one principle: <strong>write CSS around constraints and relationships rather than a collection of fixed values.</strong></p><p>The web will keep changing. Designs will become more complex, components will be reused in unexpected places, and requirements will change halfway through a project.</p><p>That’s normal.</p><p>Your CSS shouldn’t need a complete rewrite every time that happens.</p><p>Start with one technique in your next project. Apply it to a real problem, see how it behaves, and build from there.</p><p>You might be surprised by how much easier your next redesign becomes.</p><p>And if you’re still using five different margins to center one element, don’t worry.</p><p>We’ve all been there.</p><h3>Thank you for being a part of the community</h3><p><em>Before you go:</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*d9QTaaaxboQP_gKSLedW_w.png" /></figure><p>👉 Be sure to <strong>clap</strong> and <strong>follow</strong> the writer ️👏<strong>️️</strong></p><p>👉 Follow us: <a href="https://medium.com/codetodeploy"><strong>Medium</strong></a></p><p><strong>Disclosure:</strong> This post includes affiliate and partnership links.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=39a159f1142d" width="1" height="1" alt=""><hr><p><a href="https://medium.com/codetodeploy/the-css-techniques-i-ignored-as-a-beginner-but-use-in-almost-every-project-39a159f1142d">The CSS Techniques I Ignored as a Beginner but Use in Almost Every Project</a> was originally published in <a href="https://medium.com/codetodeploy">CodeToDeploy</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[I Stopped Following Every Web Development Trend — and Became a Better Developer]]></title>
            <link>https://medium.com/codetodeploy/i-stopped-following-every-web-development-trend-and-became-a-better-developer-2efce9dc16cb?source=rss-df9e89e9c50c------2</link>
            <guid isPermaLink="false">https://medium.com/p/2efce9dc16cb</guid>
            <category><![CDATA[web-development]]></category>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[programming]]></category>
            <dc:creator><![CDATA[Lubnaaly]]></dc:creator>
            <pubDate>Mon, 26 Oct 2026 09:40:14 GMT</pubDate>
            <atom:updated>2026-10-30T11:25:26.393Z</atom:updated>
            <content:encoded><![CDATA[<h4>I used to think staying ahead meant learning every new framework, library, and tool. It took real project experience to realize that becoming a better developer had very little to do with chasing trends.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*vGccSITklsx9ITg5" /><figcaption>Photo by <a href="https://unsplash.com/@leecampbell?utm_source=medium&amp;utm_medium=referral">Lee Campbell</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>read here.</p><p>Every few weeks, the web development world seems to discover something new.</p><blockquote><em>💥 </em><strong><em>Master AI &amp; Tech Skills in 3 Months!</em></strong><br>💰 One Subscription. Multiple Courses. More Savings.<br>🔥 Get Up to 50% OFF | ⏰ Limited-Time Offer<br><a href="https://trk.udemy.com/zz4NBO"><em>👉 </em><strong><em>Enroll Now &amp; Start Learning</em></strong></a></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ITogvtC6mF2BUF35Qsd9pg.png" /></figure><p>A new JavaScript framework promises better performance. A new build tool claims to make development ridiculously fast. Then another library appears, and suddenly everyone on social media seems to be using it.</p><p>I’ve been there.</p><p>I used to treat every new release like homework. If experienced developers were talking about a tool, I felt I needed to understand it. If a framework was trending on GitHub, I wanted to try it.</p><p>My browser bookmarks were full of tutorials I planned to watch someday.</p><p>Some weekends, I’d start learning a new library before finishing the project I’d started the previous weekend.</p><p>Sound familiar?</p><p>The problem wasn’t that these tools were useless. Many of them were genuinely impressive.</p><p>The problem was that I was spending more time learning what was new than understanding what I already knew.</p><p>Eventually, I changed how I approached web development.</p><p>And surprisingly, I started writing better code, debugging faster, and making more confident technical decisions.</p><p>Here’s what changed.</p><h3>1. I Realized That Knowing More Frameworks Didn’t Mean I Understood the Web Better</h3><p>For a while, I thought becoming a better web developer meant adding more technologies to my toolkit.</p><p>React? Learn it.</p><p>Vue? Worth exploring.</p><p>Svelte? Better understand how it works.</p><p>A new meta-framework? Add it to the list.</p><p>There was always something else to learn.</p><p>But I noticed something frustrating when I started building more complicated applications.</p><p>I could follow a tutorial to create a component, connect an API, and display data on a page. Yet when something behaved unexpectedly, I sometimes struggled to understand why.</p><p>The framework wasn’t necessarily the problem.</p><p>My understanding of the underlying web platform was incomplete.</p><p>For example, consider a page that makes several API requests and suddenly feels slow.</p><p>You could spend hours looking for a framework-specific optimization. Or you could investigate the actual problem:</p><ul><li>Are requests running sequentially when they could run concurrently?</li><li>Is the browser downloading unnecessarily large assets?</li><li>Is JavaScript blocking the main thread?</li><li>Is the server responding slowly?</li><li>Are repeated renders doing expensive work?</li></ul><p>These questions aren’t specific to React, Vue, or any other framework.</p><p>They’re web development questions.</p><p>Once I started investigating problems at that level, I became less dependent on whichever tool I happened to be using.</p><p><strong>The lesson:</strong> Framework knowledge helps you build applications. Understanding the browser, HTTP, JavaScript, and rendering helps you understand why those applications behave the way they do.</p><p>That second skill travels with you.</p><h3>2. I Started Learning JavaScript More Deeply Instead of Collecting Frameworks</h3><p>JavaScript is one of those languages that looks deceptively simple when you’re following tutorials.</p><p>You can build a working application without understanding every detail of how the language behaves.</p><p>Eventually, though, your application becomes complicated enough to expose the gaps.</p><p>I learned this particularly well when dealing with asynchronous operations.</p><p>Consider this example:</p><pre>async function loadDashboard() {<br>  const users = await fetch(&quot;/api/users&quot;);<br>  const orders = await fetch(&quot;/api/orders&quot;);<br>  const products = await fetch(&quot;/api/products&quot;);<br>return {<br>    users: await users.json(),<br>    orders: await orders.json(),<br>    products: await products.json()<br>  };<br>}</pre><p>The code is readable, and it works.</p><p>But there’s a potential performance problem.</p><p>Each request waits for the previous request to finish before starting. If the three requests are independent, that waiting is unnecessary.</p><p>We can run them concurrently instead:</p><pre>async function loadDashboard() {<br>  const [usersResponse, ordersResponse, productsResponse] =<br>    await Promise.all([<br>      fetch(&quot;/api/users&quot;),<br>      fetch(&quot;/api/orders&quot;),<br>      fetch(&quot;/api/products&quot;)<br>    ]);<br>const [users, orders, products] = await Promise.all([<br>    usersResponse.json(),<br>    ordersResponse.json(),<br>    productsResponse.json()<br>  ]);<br>  return { users, orders, products };<br>}Now the independent requests can run concurrently, although the actual improvement depends on network conditions, server behavior, and response sizes.</pre><p>Notice what happened here.</p><p>We didn’t change frameworks. We didn’t install another package. We didn’t rewrite the application.</p><p>We simply understood asynchronous JavaScript a little better.</p><p>Of course, this example also assumes the responses succeed. In production, I’d check response.ok and handle failures deliberately.</p><p>That detail matters, too.</p><p>The more deeply I understood JavaScript, the more often I could solve problems without reaching for another dependency.</p><p>And honestly, that felt better than finishing another beginner tutorial.</p><h3>3. I Stopped Rewriting Projects Just Because a New Tool Appeared</h3><p>This was probably one of my most expensive habits.</p><p>I’d build something using a technology I understood reasonably well. Then I’d discover a newer alternative and start wondering whether I should rebuild everything.</p><p>Maybe the new framework had a better developer experience.</p><p>Maybe the new build tool was faster.</p><p>Maybe someone on YouTube had explained why my current stack was outdated.</p><p>Before long, I was considering a rewrite that nobody had asked for.</p><p>Here’s what I eventually realized: <strong>working software doesn’t automatically need replacing just because a newer option exists.</strong></p><p>Imagine you’ve built a small e-commerce dashboard.</p><p>It works. Users can manage products, update inventory, and review orders. The code is reasonably maintainable, and the application performs well enough for its workload.</p><p>Then you discover a new framework.</p><p>Should you migrate?</p><p>Not necessarily.</p><p>I’d first ask:</p><ul><li>What specific problem does the new framework solve?</li><li>Is that problem affecting my current application?</li><li>How much migration work would be required?</li><li>Will the new architecture make future changes easier?</li><li>Can I measure a meaningful improvement?</li></ul><p>If I can’t identify a concrete benefit, the migration probably isn’t worth prioritizing.</p><p>That doesn’t mean avoiding new technologies. It means having a reason to adopt them.</p><p>Sometimes a migration is absolutely the right decision. An outdated dependency, serious performance limitations, security concerns, or an architecture that makes every change painful can justify the effort.</p><p>But novelty alone isn’t a business requirement.</p><p><strong>Pro tip:</strong> Before replacing a technology, write down the problem you’re trying to solve and how you’ll measure success. If you can’t do either, investigate further before starting the rewrite.</p><p>Your future self will thank you.</p><h3>4. I Learned to Read Documentation Before Watching Another Tutorial</h3><p>I still use tutorials.</p><p>They can be excellent for understanding unfamiliar concepts, seeing a workflow in action, or getting a project off the ground.</p><p>But I used to depend on them too much.</p><p>I’d watch someone build a feature, copy the implementation, and move on. Everything made sense while the instructor was explaining it.</p><p>Then I’d try to adapt the same code to my own project.</p><p>Suddenly, the easy part wasn’t so easy.</p><p>The tutorial assumed a different application structure. The API behaved differently. A library option had changed. My requirements didn’t match the example.</p><p>I needed to understand the tool, not just reproduce someone else’s solution.</p><p>That’s when I started spending more time with official documentation.</p><p>For example, when working with browser APIs, I’d consult the MDN Web Docs. When using a framework or library, I’d check its official documentation for supported behavior, configuration options, and limitations.</p><p>This changed how I approached unfamiliar problems.</p><p>Instead of asking, “Which tutorial shows me how to do this?” I’d ask, “What does this API actually guarantee, and what assumptions am I making?”</p><p>That question often leads to better code.</p><p>Documentation also teaches you things tutorials sometimes skip: edge cases, browser compatibility, error behavior, accessibility considerations, and performance implications.</p><p>You don’t need to memorize every API.</p><p>You need to become comfortable finding reliable answers.</p><p>That’s a much more useful skill.</p><h3>5. I Started Caring More About Browser Performance Than Developer Hype</h3><p>A development tool can feel incredibly fast while the website built with it feels slow.</p><p>These are two different things.</p><p>A fast development server makes your local workflow more pleasant. It doesn’t automatically make your production application load faster.</p><p>Similarly, a framework can offer a fantastic developer experience while your application still ships oversized JavaScript bundles, unoptimized images, or unnecessary network requests.</p><p>I started paying closer attention to what users actually experienced.</p><p>For example, when a page felt slow, I’d inspect it using Chrome DevTools rather than immediately installing another optimization library.</p><p>I’d look at:</p><ul><li>Network requests and response sizes.</li><li>JavaScript execution time.</li><li>Layout shifts and rendering behavior.</li><li>Images that were larger than necessary.</li><li>Expensive work happening on the main thread.</li></ul><p>I’d also use Lighthouse as an initial diagnostic tool and investigate the results instead of blindly accepting every recommendation.</p><p>One particularly useful concept is Core Web Vitals.</p><p>Google’s metrics include Largest Contentful Paint (LCP), Interaction to Next Paint (INP), and Cumulative Layout Shift (CLS). They help evaluate loading performance, responsiveness, and visual stability.</p><p>These metrics give developers something more useful than “the website feels kind of slow.”</p><p>They provide measurable signals to investigate.</p><p>Of course, a single Lighthouse run isn’t a complete picture of real-world performance. Lab conditions differ from actual users’ devices and networks, so field data is valuable when available.</p><p>The important shift was simple.</p><p>I stopped asking whether my stack was modern enough and started asking whether my application was delivering a good experience.</p><p>Users don’t care which framework I used.</p><p>They care whether the page loads, the buttons respond, and the checkout process works.</p><p>Fair enough.</p><h3>6. I Became More Careful About Adding Dependencies</h3><p>Every library solves a problem.</p><p>It can also introduce another dependency to maintain, update, understand, and occasionally debug.</p><p>Early on, I sometimes installed packages for tasks that required only a few lines of native JavaScript.</p><p>A utility package for a small formatting operation. Another library for something the browser already supported. Yet another dependency because a tutorial happened to use it.</p><p>Individually, these decisions seemed harmless.</p><p>Over time, they could make a project harder to maintain.</p><p>Now, before adding a dependency, I ask a few questions.</p><p><strong>First, do I actually need it?</strong></p><p>If a task is straightforward and the native solution is readable, a new package may add more complexity than value.</p><p><strong>Second, is the problem complicated enough to justify a library?</strong></p><p>Authentication, date and time handling, complex form validation, and specialized data processing can have enough edge cases to justify established solutions.</p><p>Reinventing these systems can be far more expensive than using a well-maintained package.</p><p><strong>Third, can I trust the dependency?</strong></p><p>I look at its documentation, maintenance activity, license, security history, compatibility, and whether it fits the project’s requirements.</p><p>I don’t expect every package to be perfect. I want to understand what I’m introducing into the application.</p><p>Here’s a small example.</p><p>If I need to check whether a string contains another string, JavaScript already provides includes().</p><pre>const message = &quot;Learning web development&quot;;<br>console.log(message.includes(&quot;web&quot;)); // trueInstalling a package for this would be unnecessary.</pre><p>But if I need sophisticated date-time calculations across time zones, using a reputable date-time library may be a much better decision than writing a collection of fragile custom functions.</p><p>The point isn’t to minimize dependencies at all costs.</p><p>It’s to make every dependency earn its place.</p><h3>7. I Started Finishing Projects Instead of Constantly Starting New Ones</h3><p>This change had less to do with technology and more to do with how I learned.</p><p>Starting a project is exciting.</p><p>You choose a stack, create the initial files, install dependencies, and build the first few features. Everything moves quickly because you’re working through the parts you already understand.</p><p>Then the less glamorous work begins.</p><p>You need to handle invalid input, loading states, failed requests, accessibility, testing, deployment, and unexpected user behavior.</p><p>Suddenly, that exciting weekend project requires actual engineering decisions.</p><p>I used to get distracted around this stage.</p><p>A new tool would appear, and I’d convince myself that experimenting with it was a productive use of time.</p><p>Sometimes it was.</p><p>Often, it was an excellent way to avoid finishing the difficult part of my existing project.</p><p>So I changed the rule.</p><p>Before starting another project, I try to bring the current one to a useful stopping point.</p><p>That means different things for different applications, but it generally includes:</p><ul><li>Handling common error cases.</li><li>Adding meaningful tests.</li><li>Improving accessibility and usability.</li><li>Documenting setup and configuration.</li><li>Deploying the application somewhere it can actually be used.</li></ul><p>This is where many of the most valuable lessons appear.</p><p>You discover that the elegant solution from a tutorial doesn’t handle real-world input. You find out that your deployment environment differs from your local machine. You learn that a component is difficult to test because its responsibilities are tangled together.</p><p>These are the problems that turn familiarity into experience.</p><p>A developer who has finished and maintained several applications has encountered challenges that someone who has only completed tutorials may not have seen yet.</p><p>And no new framework can give you that experience automatically.</p><h3>8. I Finally Understood That Learning Less Can Mean Learning Better</h3><p>Let me clarify something.</p><p>I’m not suggesting that web developers should ignore new technologies.</p><p>That would be a terrible strategy in a field that continues to evolve.</p><p>New frameworks, browser APIs, development tools, and architectural approaches can genuinely improve how we build software.</p><p>I still explore them.</p><p>I just don’t treat every new release as an obligation.</p><p>When I encounter an unfamiliar tool, I evaluate it based on the problems I work on, the skills I want to develop, and the value it could bring to a real project.</p><p>Sometimes I build a small proof of concept.</p><p>Sometimes I read the documentation and move on.</p><p>Sometimes I decide that the existing tool is perfectly adequate.</p><p>All three are valid outcomes.</p><p>I’ve also become more intentional about what I study.</p><p>Instead of learning a new framework every time I feel stuck, I might spend that time understanding:</p><ul><li>How HTTP caching works.</li><li>Why browser rendering can become expensive.</li><li>How JavaScript’s event loop affects responsiveness.</li><li>How to design APIs with predictable error handling.</li><li>How to test asynchronous code reliably.</li><li>How to structure applications so that individual components are easier to maintain.</li></ul><p>These concepts aren’t always exciting to post about on social media.</p><p>But they’re useful in almost every serious web development project.</p><p>And the knowledge compounds.</p><p>When you understand the browser, a new framework becomes easier to evaluate. When you understand asynchronous JavaScript, a new data-fetching library becomes easier to reason about. When you understand testing, a new architecture becomes easier to maintain.</p><p>Fundamentals don’t eliminate the need to learn new tools.</p><p>They make learning new tools much easier.</p><h3>My Simple Rule for Evaluating Web Development Trends</h3><p>Whenever I encounter a new framework, library, or development tool, I try to answer three questions:</p><p><strong>1. What problem does it solve?</strong></p><p>I want a clear explanation beyond “it’s modern” or “everyone is using it.”</p><p><strong>2. Is that problem relevant to my work?</strong></p><p>A tool can be excellent and still be unnecessary for my current projects.</p><p><strong>3. Is the benefit worth the learning and maintenance cost?</strong></p><p>Every new technology requires some investment. I want that investment to produce a meaningful return.</p><p>If a tool passes these questions, I’ll happily explore it.</p><p>If it doesn’t, I don’t feel guilty about skipping it.</p><p>There will always be another release, another announcement, and another developer insisting that the old way is dead.</p><p>You don’t have to attend every technological parade.</p><p>Sometimes, you just need to finish building your application.</p><h3>Final Thoughts</h3><p>I used to measure my growth as a web developer by how many technologies I knew.</p><p>Now, I pay more attention to how well I can solve problems.</p><p>Can I identify the real cause of a performance issue?</p><p>Can I debug a complicated failure without randomly changing code until something works?</p><p>Can I choose an appropriate architecture without overengineering a simple application?</p><p>Can I explain why I selected a particular library instead of simply saying it’s popular?</p><p>Those questions tell me much more about my development skills than the number of frameworks I’ve tried.</p><p>The web development ecosystem will continue to change. Some tools will become essential, others will disappear, and a few will solve problems we haven’t encountered yet.</p><p>I’ll keep learning.</p><p>I’ll keep experimenting.</p><p>But I no longer want to spend every week chasing something new while neglecting the skills that make me effective.</p><p><strong>The goal isn’t to know every tool in web development. It’s to know enough to choose the right tool, understand what it does, and build something that works reliably.</strong></p><p>That’s what made me a better developer.</p><p>Not following every trend.</p><p>Learning when a trend actually matters.</p><p>And sometimes, having the confidence to close the tutorial tab and finish the project I already started.</p><h3>Thank you for being a part of the community</h3><p><em>Before you go:</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*d9QTaaaxboQP_gKSLedW_w.png" /></figure><p>👉 Be sure to <strong>clap</strong> and <strong>follow</strong> the writer ️👏<strong>️️</strong></p><p>👉 Follow us: <a href="https://medium.com/codetodeploy"><strong>Medium</strong></a></p><p><strong>Disclosure:</strong> This post includes affiliate and partnership links.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2efce9dc16cb" width="1" height="1" alt=""><hr><p><a href="https://medium.com/codetodeploy/i-stopped-following-every-web-development-trend-and-became-a-better-developer-2efce9dc16cb">I Stopped Following Every Web Development Trend — and Became a Better Developer</a> was originally published in <a href="https://medium.com/codetodeploy">CodeToDeploy</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The Biggest Lessons I Learned After Years of Building Websites]]></title>
            <link>https://medium.com/codetodeploy/the-biggest-lessons-i-learned-after-years-of-building-websites-56cac1629ae2?source=rss-df9e89e9c50c------2</link>
            <guid isPermaLink="false">https://medium.com/p/56cac1629ae2</guid>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[web-development]]></category>
            <category><![CDATA[machine-learning]]></category>
            <dc:creator><![CDATA[Lubnaaly]]></dc:creator>
            <pubDate>Wed, 21 Oct 2026 14:20:05 GMT</pubDate>
            <atom:updated>2026-10-21T20:11:36.061Z</atom:updated>
            <content:encoded><![CDATA[<h4><em>The mistakes, unexpected challenges, and practical lessons that changed how I approach web development.</em></h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*dxaxj9vN046CbDyp" /></figure><p>read here.</p><p>When I first started building websites, I thought becoming a better developer meant learning more technologies.</p><blockquote><em>💥 </em><strong><em>Master AI &amp; Tech Skills</em></strong><br>💰 One Plan. Multiple Courses. More Savings.<br>🔥 Get Up to 50% OFF | ⏰ Limited-Time Offer<br><a href="http://trk.udemy.com/OYJoKA"><em>👉 </em><strong><em>Enroll Now &amp; Start Learning</em></strong></a></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ITogvtC6mF2BUF35Qsd9pg.png" /></figure><p>A new JavaScript framework? Interesting.</p><p>A faster CSS technique? I was in.</p><p>A new backend library? Time to experiment.</p><p>I spent plenty of time learning tools, trying frameworks, and looking for better ways to write code.</p><p>But after working on different kinds of web projects, I realized something unexpected.</p><p><strong>Knowing how to build a website and knowing how to build a good website are two very different skills.</strong></p><p>A page can look perfect on your laptop and break on a mobile phone. An API can work beautifully with ten users and struggle when hundreds of requests arrive simultaneously. A feature can take an hour to implement and three days to maintain.</p><p>These are the problems that tutorials don’t always prepare you for.</p><p>Here are the biggest lessons I’ve learned from building websites — and how they changed the way I approach development.</p><h3>1. Clean Code Matters More When You Come Back to It</h3><p>When you’re working on a small project, messy code doesn’t feel like a problem.</p><p>You know where everything is. You remember why you wrote a particular function. If something breaks, you can probably fix it in a few minutes.</p><p>Then the project grows.</p><p>Suddenly, one component handles authentication, form validation, API requests, and half the application’s business logic.</p><p>Changing one thing breaks something else.</p><p>I’ve learned that code isn’t just written for computers. It’s also written for the developer who has to understand it later.</p><p>Imagine building a registration form. You could put every validation rule directly inside the component. That might work initially, but adding password requirements, reusable error messages, and additional registration methods makes the component increasingly difficult to maintain.</p><p>Separating validation logic into its own function gives you a much cleaner starting point.</p><p>The same principle applies to backend services, database queries, and API integrations.</p><p><strong>My rule now: If a function needs a long explanation before someone can understand it, I look for a simpler design.</strong></p><p>You don’t need to abstract everything. You just need to make future changes less painful.</p><h3>2. Responsive Design Is Not Something You Add at the End</h3><p>One of the easiest mistakes in web development is designing for desktop first and treating mobile support as a final adjustment.</p><p>I’ve learned that this approach often creates unnecessary work.</p><p>A navigation bar that looks elegant on a wide screen might become unusable on a phone. A three-column dashboard might turn into a horizontal scrolling nightmare. Buttons that look perfectly spaced on desktop can become frustratingly difficult to tap.</p><p>The problem isn’t always the CSS.</p><p>Sometimes the layout itself needs a different approach.</p><p>For example, CSS Grid can handle a responsive card layout without requiring separate media queries for every possible screen width.</p><pre>.card-grid {<br>    display: grid;<br>    grid-template-columns: repeat(<br>        auto-fit,<br>        minmax(min(100%, 250px), 1fr)<br>    );<br>    gap: 1rem;<br>}</pre><p>This lets the browser fit as many cards as the available space allows while keeping each card at a reasonable minimum width.</p><p>Instead of manually defining every layout change, you let CSS handle much of the work.</p><p>I also test forms, navigation menus, tables, and modals on smaller screens early in development.</p><p><strong>The lesson:</strong> Responsive design is an architectural decision, not a finishing touch.</p><h3>3. The Happy Path Is Not Enough</h3><p>It’s easy to build a feature that works when everything goes right.</p><p>A user submits valid information. The server responds immediately. The database connection works. Nothing times out.</p><p>Real applications aren’t that polite.</p><p>Users submit incomplete forms. Networks fail. APIs return unexpected responses. Someone clicks a button twice because the first click appears to do nothing.</p><p>And suddenly, your perfectly working feature isn’t so perfect.</p><p>Consider a form that sends data to an API. A basic implementation might show a success message when the request finishes. A more reliable implementation also handles loading, validation errors, network failures, and duplicate submissions.</p><p>For example:</p><pre>async function submitForm(data) {<br>    try {<br>        const response = await fetch(&quot;/api/submit&quot;, {<br>            method: &quot;POST&quot;,<br>            headers: {<br>                &quot;Content-Type&quot;: &quot;application/json&quot;<br>            },<br>            body: JSON.stringify(data)<br>        });<br>if (!response.ok) {<br>            throw new Error(<br>                `Request failed: ${response.status}`<br>            );<br>        }<br>        return await response.json();<br>    } catch (error) {<br>        console.error(&quot;Submission failed:&quot;, error);<br>        throw error;<br>    }<br>}This doesn’t handle every production concern, but it makes an important distinction: a failed HTTP response is not the same as a successful one.</pre><p>Depending on the application, you may also need request timeouts, retries, idempotency keys, and user-friendly error messages.</p><p>The most important question isn’t simply, “Does this work?”</p><p>It’s:</p><p><strong>“What happens when this stops working?”</strong></p><p>That question has helped me think more like an engineer and less like someone who is only trying to finish a feature.</p><h3>4. Performance Problems Often Start With Small Decisions</h3><p>You don’t always need a complicated optimization strategy to make a website faster.</p><p>Sometimes the biggest improvements come from fixing a few ordinary mistakes.</p><p>An oversized image can slow down an otherwise lightweight landing page. An API endpoint that performs repeated database queries can make a dashboard feel sluggish. Loading an entire JavaScript library for one small utility can increase the amount of code a browser needs to download.</p><p>The challenge is identifying which problem actually matters.</p><p>For frontend performance, I start by checking image sizes, unnecessary JavaScript, rendering behavior, and network requests.</p><p>For backend performance, I look at database query counts, slow endpoints, response payload sizes, and external API calls.</p><p>Tools such as browser developer tools, Lighthouse, and Python’s profiling utilities can help identify bottlenecks.</p><p>I try not to optimize based on intuition alone.</p><p>For example, if a dashboard takes three seconds to load, rewriting the frontend might accomplish very little if most of that time is spent waiting for an inefficient database query.</p><p><strong>Measure first. Optimize second.</strong></p><p>A small improvement in the right place can be more valuable than a complete rewrite of the wrong component.</p><h3>5. Database Design Can Make or Break Your Application</h3><p>When building a new feature, it’s tempting to focus on the interface first.</p><p>You create the form, connect the API, and store whatever data the frontend sends.</p><p>That approach works until the application needs more complicated queries, relationships, reporting, or data integrity.</p><p>Consider an e-commerce website.</p><p>If customer details, order information, and product data are all stored in one poorly structured table, updating a product name or retrieving a customer’s order history can become unnecessarily complicated.</p><p>A more thoughtful schema separates customers, orders, and products, then connects them through appropriate relationships.</p><p>Indexes can improve query performance, but adding indexes everywhere isn’t the answer either. They consume storage and can make writes more expensive.</p><p>The right design depends on how the application reads and writes its data.</p><p>I also pay attention to constraints, transactions, and migrations. These aren’t glamorous features, but they help protect the application from inconsistent data and risky deployments.</p><p>A beautiful interface cannot compensate for unreliable data underneath it.</p><h3>6. Good APIs Make Frontend Development Easier</h3><p>I’ve come to appreciate how much time a well-designed API can save across an entire project.</p><p>When an API has inconsistent response formats, unclear error messages, and unpredictable naming conventions, the frontend becomes responsible for compensating for those decisions.</p><p>Every endpoint starts to require its own special handling.</p><p>A more consistent API might return data in a predictable structure, use appropriate HTTP status codes, validate incoming requests, and document its expected inputs.</p><p>For example, a Python backend built with FastAPI can use Pydantic models to validate request data and generate an OpenAPI specification.</p><p>That makes it easier for frontend developers to understand what an endpoint expects without repeatedly inspecting backend code.</p><p>The same principle applies when working with external APIs.</p><p>I pay particular attention to authentication, rate limits, pagination, timeouts, and error handling. These details often determine whether an integration remains dependable after the initial demonstration.</p><p>A useful API isn’t merely one that returns the correct data.</p><p>It’s one that other parts of the system can depend on.</p><h3>7. Testing Saves More Time Than It Costs</h3><p>I used to think of testing as something that became important once a project grew large enough.</p><p>Now I see it differently.</p><p>Even small applications develop complicated behavior as features interact.</p><p>Imagine changing a shared authentication function. The change might fix one login issue but accidentally prevent password resets or break access to a protected page.</p><p>Without tests, you may discover the problem only after someone encounters it.</p><p>You don’t need thousands of tests to benefit from testing.</p><p>Start with the logic that would be expensive or embarrassing to break:</p><ul><li>Form validation and business rules.</li><li>Authentication and authorization.</li><li>Important API endpoints.</li><li>Database operations.</li><li>Critical user journeys.</li></ul><p>For Python applications, pytest is an excellent starting point. For browser-level testing, tools such as Playwright can verify that users can complete important workflows.</p><p>I also distinguish between testing individual functions and testing how the entire application behaves. Both are useful, but they catch different kinds of problems.</p><p>A test suite won’t guarantee that your application has no bugs.</p><p>It does make many changes safer because you can catch regressions before they reach users.</p><h3>8. Deployment Is Part of Development</h3><p>A feature that works locally is not necessarily ready for production.</p><p>Your local machine may have environment variables that the server doesn’t. Your development database may contain data that doesn’t exist in production. File paths, permissions, dependencies, and network settings can all behave differently.</p><p>I’ve learned to treat deployment requirements as part of the project from the beginning.</p><p>That means keeping secrets out of source control, documenting environment variables, managing dependencies, and creating repeatable deployment steps.</p><p>It also means thinking about database migrations, logging, backups, and rollback strategies before they become urgent.</p><p>For applications that need them, health checks and monitoring help reveal problems that ordinary manual testing won’t catch.</p><p>There’s a big difference between deploying an application once and being able to deploy it reliably every week.</p><p>The second requires a process, not just a collection of terminal commands.</p><h3>9. The Simplest Solution Is Often the Best One</h3><p>New frameworks and architectural patterns can be exciting.</p><p>It’s tempting to use microservices for a small application, introduce a complex state-management library for a handful of components, or build an AI agent when a regular API call would do the job.</p><p>I’ve learned to resist that temptation.</p><p>Every additional dependency, abstraction, and service introduces another thing to understand, maintain, test, and potentially debug.</p><p>A straightforward application with clear boundaries may be a better choice than a complicated architecture designed for problems you don’t have yet.</p><p>This doesn’t mean avoiding advanced tools.</p><p>It means earning the complexity.</p><p>If background processing becomes necessary, introduce a task queue. If database queries become a bottleneck, profile them and consider indexing or caching. If separate services solve a genuine scaling or organizational problem, then consider splitting the application.</p><p><strong>Pro tip:</strong> Don’t ask whether an architecture is sophisticated enough. Ask whether it solves the problem more reliably than a simpler alternative.</p><p>That mindset has helped me avoid plenty of unnecessary engineering.</p><h3>10. Communication Is a Technical Skill, Too</h3><p>Not every web development problem can be solved by writing more code.</p><p>Sometimes the real problem is an unclear requirement.</p><p>A client asks for a “fast dashboard,” but nobody defines what fast means. A designer expects a particular interaction that isn’t included in the specification. A teammate changes an API response without realizing another service depends on it.</p><p>These misunderstandings create rework.</p><p>Before implementing a feature, I try to clarify what success looks like, what the edge cases are, and which constraints matter most.</p><p>For a dashboard, that might mean agreeing on which metrics appear, how often the data refreshes, and how the page should behave when no data is available.</p><p>For a payment flow, it means discussing failed transactions, duplicate submissions, and confirmation states — not just the successful checkout.</p><p>Clear documentation and small, focused pull requests also make collaboration easier.</p><p>Good developers solve technical problems.</p><p>Great engineering teams also make sure they’re solving the right problems.</p><h3>The Biggest Lesson of All</h3><p>Looking back, the most important improvements in my web development workflow haven’t come from learning every new framework.</p><p>They’ve come from changing the questions I ask.</p><p>Instead of asking whether a feature works, I ask how it behaves when something fails.</p><p>Instead of asking whether code is clever, I ask whether another developer can maintain it.</p><p>Instead of asking whether an application runs locally, I ask whether it can be deployed, monitored, and updated safely.</p><p>And instead of adding another library, I ask whether the existing solution is actually insufficient.</p><p>These questions sound simple, but they change how you approach almost every project.</p><blockquote><em>“Simplicity boils down to two steps: Identify the essential. Eliminate the rest.” — Leo Babauta</em></blockquote><p>You don’t need to know every framework to become a better web developer.</p><p>You need to understand the problems your applications face, recognize the trade-offs behind your decisions, and learn from the mistakes you make along the way.</p><p>Because writing code is only one part of building software.</p><p>The real skill is building something that continues to work after the tutorial ends, the requirements change, and real users start depending on it.</p><h3>Thank you for being a part of the community</h3><p><em>Before you go:</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*d9QTaaaxboQP_gKSLedW_w.png" /></figure><p>👉 Be sure to <strong>clap</strong> and <strong>follow</strong> the writer ️👏<strong>️️</strong></p><p>👉 Follow us: <a href="https://medium.com/codetodeploy"><strong>Medium</strong></a></p><p>👉 CodeToDeploy Tech Community is live on Discord — <a href="https://discord.gg/ZpwhHq6D"><strong>Join now!</strong></a></p><p><strong>Disclosure:</strong> This post includes affiliate and partnership links.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=56cac1629ae2" width="1" height="1" alt=""><hr><p><a href="https://medium.com/codetodeploy/the-biggest-lessons-i-learned-after-years-of-building-websites-56cac1629ae2">The Biggest Lessons I Learned After Years of Building Websites</a> was originally published in <a href="https://medium.com/codetodeploy">CodeToDeploy</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[I Thought Web Development Was Easy Until I Built My First Real Project]]></title>
            <link>https://medium.com/codetodeploy/i-thought-web-development-was-easy-until-i-built-my-first-real-project-58e800f00f4f?source=rss-df9e89e9c50c------2</link>
            <guid isPermaLink="false">https://medium.com/p/58e800f00f4f</guid>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[web-development]]></category>
            <category><![CDATA[python]]></category>
            <dc:creator><![CDATA[Lubnaaly]]></dc:creator>
            <pubDate>Wed, 21 Oct 2026 14:10:33 GMT</pubDate>
            <atom:updated>2026-10-21T20:12:26.546Z</atom:updated>
            <content:encoded><![CDATA[<h4>Tutorials made web development look simple. Building something real taught me what it actually takes to create software that works.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/0*g5Cx96Wg37zFsNmX" /></figure><p>read here.</p><h3>The Article Outline</h3><h3>1. Everything Looked Easy in Tutorials</h3><p>Start with the excitement of learning HTML, CSS, and JavaScript. A few tutorials later, you can build a landing page, add buttons, and make things look reasonably good.</p><blockquote><em>💥 </em><strong><em>Master AI &amp; Tech Skills</em></strong><br>💰 One Plan. Multiple Courses. More Savings.<br>🔥 Get Up to 50% OFF | ⏰ Limited-Time Offer<br><a href="http://trk.udemy.com/OYJoKA"><em>👉 </em><strong><em>Enroll Now &amp; Start Learning</em></strong></a></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ITogvtC6mF2BUF35Qsd9pg.png" /></figure><p>Then you decide to build something from scratch.</p><p>That’s when things get interesting.</p><h3>2. My First Real Project Exposed Everything I Didn’t Know</h3><p>Explore the problems that tutorials often simplify:</p><ul><li>Making a responsive layout work across different screen sizes.</li><li>Handling forms, validation, and unexpected user input.</li><li>Connecting a frontend to a backend API.</li><li>Managing loading states, errors, and missing data.</li><li>Debugging code that works perfectly in one situation but fails in another.</li></ul><h3>3. The Frontend Was Only Half the Problem</h3><p>Explain why a polished interface is not the same as a complete application. Real projects require data persistence, authentication when needed, API integration, security, and thoughtful error handling.</p><h3>4. Debugging Became a Bigger Skill Than Writing Code</h3><p>Share how reading error messages, using browser developer tools, inspecting network requests, and isolating bugs became essential parts of development.</p><h3>5. I Started Understanding Why Developers Care About Code Quality</h3><p>Introduce practical lessons about reusable components, clear naming, separating responsibilities, Git, environment variables, and testing.</p><p>The key insight: code that works today should also be understandable and maintainable tomorrow.</p><h3>6. What I Would Do Differently Today</h3><p>Offer practical advice for developers starting their first serious project:</p><ul><li>Build something small that solves a real problem.</li><li>Finish one complete feature before adding another.</li><li>Learn browser developer tools early.</li><li>Handle errors instead of assuming everything will work.</li><li>Deploy the application before considering it finished.</li><li>Treat documentation and debugging as part of development, not interruptions.</li></ul><h3>Final Thoughts</h3><p>The biggest lesson wasn’t that web development was impossibly difficult. It was that building a working application involves much more than writing code that looks right.</p><p><strong>Tutorials taught me how to make things work. My first real project taught me how to make them keep working.</strong></p><h3>Thank you for being a part of the community</h3><p><em>Before you go:</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*d9QTaaaxboQP_gKSLedW_w.png" /></figure><p>👉 Be sure to <strong>clap</strong> and <strong>follow</strong> the writer ️👏<strong>️️</strong></p><p>👉 Follow us: <a href="https://medium.com/codetodeploy"><strong>Medium</strong></a></p><p>👉 CodeToDeploy Tech Community is live on Discord — <a href="https://discord.gg/ZpwhHq6D"><strong>Join now!</strong></a></p><p><strong>Disclosure:</strong> This post includes affiliate and partnership links.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=58e800f00f4f" width="1" height="1" alt=""><hr><p><a href="https://medium.com/codetodeploy/i-thought-web-development-was-easy-until-i-built-my-first-real-project-58e800f00f4f">I Thought Web Development Was Easy Until I Built My First Real Project</a> was originally published in <a href="https://medium.com/codetodeploy">CodeToDeploy</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>