Secure Your Local Development: Generate HTTPS Certificates for .test or .internal Domains

In today’s web development landscape, HTTPS isn’t just for production—it’s a necessity for local development and testing. Modern browsers enforce strict security policies, and features like authentication cookies or service workers require HTTPS even during development. Using localhost works, but it often comes with hidden restrictions. Instead, adopting a dedicated local development domain (like .test or .internal) with HTTPS ensures a production-like environment and avoids browser quirks. Here’s how to set it up.

Why HTTPS Matters in Development

  1. Browser Requirements : Features like secure cookies, geolocation, or service workers often require HTTPS, even locally.
  2. Real-World Parity : Developing with HTTPS mirrors production environments, reducing deployment surprises.
  3. Security Compliance : Authentication cookies marked Secure won’t work over HTTP, breaking login flows during testing.

Avoid These Domains :

  • .dev and .app : Owned by Google, these enforce HTTPS via HSTS preloading. Self-signed certs will fail.
  • .local : Reserved for multicast DNS (mDNS) and treated as non-standard by browsers like Safari.

Use These Instead :

  • .test and .internal : Reserved by RFC 6762 for local testing. No risk of colliding with real domains.

How to generate a new key and certificate

openssl req -x509 -nodes -newkey rsa:2048 -keyout local.test.key -out local.test.crt -config openssl_test.cnf -extensions v3_req -days 9999

Ensure you create openssl_test.cnf file with the content below

[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
prompt = no

[req_distinguished_name]
CN = local.test

[v3_req]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @alt_names

[alt_names]
DNS.1 = local.test
DNS.2 = *.local.test

The above will create a key and a wild card certificate.

Add the certificate above to your operating system’s trust store

MacOS

1. Open Keychain Access.

2. Drag local.test.crt into the System keychain.

3. Double-click the certificate → Expand Trust → Set When using this certificate to Always Trust. <- important

Windows

1. Double-click local.test.crt.

2. Go to Install Certificate → Local Machine → Place all certificates in the following store → Trusted Root Certification Authorities.

Linux

# Copy the certificate to the trusted store
sudo cp local.test.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates

Add the certificate above to your operating system’s trust store

For dotnet app, add this to appsettingsDevelopment.json

 "Kestrel": {
    "Endpoints": {
      "Https": {
        "Url": "https://your-app-name.local.test:5033", 
        "Certificate": {
          "Path": "/path/to/local.test.crt",
          "KeyPath": "/path/to/local.test.key"
        }
      }
    }
  }

Resolving domain name

Add the line below to you OS hosts file

127.0.0.1   your-app-name.local.test

hosts file on MacOS is at /etc/hosts

Important

Do not expose .key file as may compromised your system

Embracing Clean Code: Why Quick Hacks Are OK—But Only Temporarily


Introduction

If you’ve been coding for more than a minute, you know the feeling: you’re under pressure to get a feature out ASAP or fix a critical bug. In these scenarios, writing quick-and-dirty code can seem like the fastest way to save the day. But here’s the catch: leaving that messy code forever will come back to haunt you (and everyone else on your team).

In this blog post, we’ll explore why it’s sometimes acceptable—even necessary—to write unpolished code quickly, and why it’s not acceptable to leave your codebase in a perpetual state of chaos. We’ll also share practical tips on how to review, refactor, and keep your code clean—including a few examples of what “messy” versus “clean” code might look like.


When Is Quick-and-Dirty Code OK?

  1. Prototyping & MVPs
    Sometimes you just need to test the waters or gather feedback. In these cases, rapid development can help you validate your ideas without spending weeks perfecting every function or logic flow.
  2. Emergency Bug Fixes
    If your production system is down, downtime costs money. Addressing the problem fast might override the desire for perfect code in that moment.
  3. Tight Deadlines
    Some projects have fixed timelines. If you’re at the 11th hour, you might sacrifice neatness for delivery.

The catch? This kind of code should be a short-term solution—like a bandage, not a permanent fix.


The Downside of Leaving Messy Code

  1. Maintainability
    If your codebase is a tangled mess, even the simplest updates can be time-consuming and error-prone.
  2. Scalability
    When it’s time to add new features, messy code can make integration painful, risking bugs and unpredictable behavior.
  3. Technical Debt
    Untidy code is essentially a loan you take against future development time. The “interest” is the extra work your team will have to do later to unravel the mess.

What Does “Clean Code” Look Like?

Clean code doesn’t necessarily mean “fancy” code. It’s code that’s:

  • Easy to read: Well-named variables, functions, and classes.
  • Easy to maintain: Logical structure, single-purpose methods, and fewer nested conditions.
  • Consistent: Uses uniform formatting and follows language/team guidelines.

Below, you’ll find some practical examples of transforming messy code into cleaner versions, primarily in C#. Even if you code in another language, the principles still apply.


Example 1: Loop Body as a Single Method

Messy Approach

for (int i = 0; i < items.Count; i++)
{
    // Validation logic
    if (items[i] == null || !items[i].IsValid())
    {
        // handle error
    }

    // Processing logic
    items[i].ProcessStep1();
    items[i].ProcessStep2();
    if (items[i].NeedsExtraStep)
    {
        // extra step
    }

    // Save results
    Save(items[i]);
}

Here, the loop contains multiple responsibilities: validation, processing, saving, etc. It’s not clear at a glance what the loop is really doing.

Clean Approach

for (int i = 0; i < items.Count; i++)
{
    ProcessItem(items[i]);
}

private void ProcessItem(Item item)
{
    if (!IsValid(item))
    {
        // handle error or early return
        return;
    }

    item.ProcessStep1();
    item.ProcessStep2();

    if (item.NeedsExtraStep)
    {
        // handle extra step
    }

    Save(item);
}

By moving the loop body into ProcessItem(), you encapsulate the logic into a single, well-named method. The loop itself becomes easier to read, and ProcessItem is more maintainable.


Example 2: Early Returns to Reduce Nesting

Messy Approach (deep nesting):

public string GetStatus(User user)
{
    string result = string.Empty;
    if (user != null)
    {
        if (user.IsActive)
        {
            if (!string.IsNullOrEmpty(user.Status))
            {
                result = user.Status;
            }
            else
            {
                result = "Status not set";
            }
        }
        else
        {
            result = "Inactive";
        }
    }
    else
    {
        result = "Unknown user";
    }
    return result;
}

Clean Approach (early exits):

public string GetStatus(User user)
{
    if (user == null) return "Unknown user";
    if (!user.IsActive) return "Inactive";
    if (string.IsNullOrEmpty(user.Status)) 
        return "Status not set";

    return user.Status;
}

By returning early from each “failure” or edge case, we avoid deeply nested logic. The method now reads like a straightforward checklist.


Example 3: Collapsing If Statements

Messy Approach:

if (user != null)
{
    if (user.HasAccess)
    {
        if (user.HasValidLicense)
        {
            // do something
        }
    }
}

Clean Approach:

if (user == null) return;
if (!user.HasAccess) return;
if (!user.HasValidLicense) return;

// do something

Alternatively, if these conditions are truly all-or-nothing:

if (user != null && user.HasAccess && user.HasValidLicense)
{
    // do something
}

Either way, you reduce nesting and make the code easier to follow.


Example 4: Meaningful Naming

Messy Approach:

public void DoStuff(int a, int b)
{
    var x = a + b;
    Console.WriteLine($"Sum is {x}");
}

Clean Approach:

public void PrintSum(int firstNumber, int secondNumber)
{
    var sum = firstNumber + secondNumber;
    Console.WriteLine($"Sum is {sum}");
}

A small change in naming can make a big difference in clarity.


The Role of Code Review

Peer Review

  • Fresh Eyes: Colleagues often catch issues or suggest improvements you’ve overlooked.
  • Standards Enforcement: Ensures everyone follows the same coding conventions.
  • Skill-Sharing: It’s an opportunity for the team to learn from each other.

Self Review

  • Set It Aside: Taking a break before reviewing your own code can reveal glaring mistakes you were blind to in the moment.

Automated Tools

  • Linters & Formatters: Tools like StyleCop (C#) or Prettier (JS) fix common style problems automatically.
  • Static Analysis: CI integrations (SonarQube, etc.) can catch code smells and potential bugs before they make it into production.

Techniques for Code Cleanup & Refactoring

  1. Incremental Refactoring
    You don’t need a massive rewrite. Improve code gradually. For example, if you see messy logic in one function, refactor just that part.
  2. Small, Focused Pull Requests
    Don’t mix big refactorings with new features in the same PR. Keep changes logical and cohesive.
  3. Fix It When You Touch It
    If you’re working in a file and see messy code nearby, take a few extra minutes to clean it up. Over time, these minor improvements will have a major impact on the overall quality of your codebase.

Conclusion

  • Short-Term Hacks: Perfectly fine for prototypes, emergencies, and strict deadlines.
  • Long-Term Solutions: Make sure you review that code soon after and refactor it to maintain a healthy codebase.
  • Ongoing Maintenance: Regular code reviews, consistent styling, and small refactoring efforts keep your project agile and future-friendly.

Writing messy, rapid-fire code might solve problems quickly, but leaving it unrefined can create exponentially bigger issues down the line. Instead, embrace the idea that quick and bad is a temporary state, not a final destination. Through code reviews, incremental refactoring, and a commitment to clean code principles, you’ll build software that’s both delivered on time and maintainable for the future.

Remember: Code cleanup is an investment. The few minutes you spend now will save hours—if not days—of frustration down the road.

Do not returns null

If you create a method that return a list and you don’t have data, always return empty list instead of null. This would allow the caller to iterate the list without checking. As an example, consider the 2 blocks of code below, which is better?

var customers = GetCustomers();
foreach(var customer in customers) {
  // do something
}
var customers = GetCustomers();
if (customers != null) {
  foreach(var customer in customers) {
    // do something
  }
}

Having said that, if you write caller side code and received a list. Make sure you don’t get NullReferenceException by using this code.

var customers = GetCustomers();
foreach(var customer in customers ?? Array.Empty<Customer>()) {
  // do something
}

C# Exception Handler

Given the code below, how would you add exception handling in your code?

public void ButtonClicked() {
   DoSomething();
   DoTask2();
}

private void DoSomething() {
  DoSomethingElse();
}

private void DoSomethingElse() {
  // code
}

private void DoTask2() {
  // code
}

Some developer would handle like this

public void ButtonClicked() {
  try {
    DoSomething();
    DoTask2();
  }
  catch(Exception ex) {
    logger.LogError(ex, "Error in buttonclicked");
  }
}

private void DoSomething() {
  DoSomethingElse();
}

private void DoSomethingElse() {
  // code
}

private void DoTask2() {
  // code
}

While other do something like this

public void ButtonClicked() {
   var result = DoSomething();
   if (result.Success) {
     var task2Result = DoTask2();
     if (!task2Result.Success) {
       logger.logError(result.Error);
     }
   }
   else {
     logger.logError(result.Error);
   }
}

private Result DoSomething() {
  try {
    DoSomethingElse();
  }
  catch(Exception ex) {
    return new Result { Success = false, Error = ex.Message }
  }
}

private result DoSomethingElse() {
  try {
    // code
  }
  catch(Exception ex) {
    return new Result { Success = false, Error = ex.Message }
  }
}

private void DoTask2() {
  try {
    // code
  }
  catch(Exception ex) {
    return new Result { Success = false, Error = ex.Message }
  }
}

public class Result 
{
   public bool Success { get; set; }
   public string Error { get; set; }
}

Which style is more readable?

Startup configure method

Did you know that you can inject any object in the Configure method of your startup class? For example if you want to inject ICustomer.

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddScoped<ICustomer,Customer>();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ICustomer customer)
    {
        // use customer here
    }
}

Date and time arithmetics

One of our developer wrote this code

private DateTimeOffset CombineAppointmentDateTime(string date, string time)
{
    time = time == "NA" ? "00:00" : (time == "9:30" ? "09:30" : time);
    var dateTime = DateTime.ParseExact($"{date} {time}", 
                       Constants.DateTimeFormat, CultureInfo.InvariantCulture);
    return DateTime.SpecifyKind(dateTime, DateTimeKind.Local);
}

var result = CombineAppointmentDateTime(
    DateTime.Now.ToString("yyyy-MM-dd"), "13:00");

He is combining the date and time but in a clumsy way. What he’s doing is composing a string then convert it to DateTime. A lot of junior developers do this way for some reasons.

What a lot of developers tend to forget is that using date and time arithmetics is much more effiencient and readable.

The code below is much better.

private DateTimeOffset CombineAppointmentDateTime(DateTime date, string time)
{
    return TimeSpan.TryParse(time, out TimeSpan val)
           ? date.Date + val
           : date.Date;
}

var result = CombineAppointmentDateTime(DateTime.Now,"13:00");