How to Install NGINX, PHP & MySQL on Amazon Linux 2023 (Step-by-Step LEMP Guide)

Setting up a high-performance web server stack using NGINX, PHP, and MySQL on Amazon Linux 2023 is a popular choice for developers deploying applications on AWS EC2.

This complete guide walks you through installing and configuring the LEMP stack (Linux, NGINX, MySQL, PHP) step by step.

Prerequisites

  • Amazon EC2 instance running Amazon Linux 2023
  • SSH access to your server
  • Basic command-line knowledge

Step 1: Install NGINX

Start by updating your system and installing NGINX.

sudo dnf update
sudo dnf install -y nginx
sudo systemctl start nginx.service
sudo systemctl status nginx.service
sudo systemctl enable nginx.service

Once installed, open your browser and enter your server’s public IP to confirm NGINX is running.

Step 2: Install PHP

Next, install PHP on your server.

sudo dnf install -y php
php -v

To optimize performance, update the memory_limit inside /etc/php.ini.

Tip:

You can locate your PHP configuration file using phpinfo().

Step 3: Install MySQL

Now let’s install MySQL server.

sudo dnf install wget
sudo wget https://dev.mysql.com/get/mysql80-community-release-el9-3.noarch.rpm
sudo dnf install mysql80-community-release-el9-3.noarch.rpm
sudo dnf update
sudo dnf install mysql-community-server
sudo systemctl start mysqld
sudo systemctl enable mysqld
sudo systemctl status mysqld

Secure MySQL Installation

sudo grep 'temporary password' /var/log/mysqld.log
sudo mysql_secure_installation -p
mysql -u root -p

Fix PHP MySQL Connection Issue

If PHP cannot connect to MySQL, install the MySQL driver:

sudo yum -y install php-mysqlnd

Step 4: Configure NGINX for Your Domain

Ensure your domain is pointed to your EC2 instance using an A record.

Create NGINX Config File

cd /etc/nginx
cd conf.d
sudo nano mysite.conf

Paste the following configuration:

server {
    listen 80;
    server_name mysite.com;
    root /var/www/html/mysite.com;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";

    index index.php;

    charset utf-8;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php-fpm/www.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}

Restart NGINX

sudo systemctl restart nginx

Test Your Setup

Create an index.php file inside:

/var/www/html/mysite.com

Then open your domain in a browser.

Permissions Setup

Set correct ownership for your web directory:

sudo chown -R ec2-user:apache /var/www/html

Laravel Storage Permissions

sudo chmod -R 777 /var/www/html/storage/*

If needed, apply permissions to other directories:

sudo chmod -R 777 /path/to/directory

Conclusion

You have successfully installed and configured NGINX, PHP, and MySQL on Amazon Linux 2023. This LEMP stack setup is optimized for performance and scalability, making it ideal for hosting modern web applications like Laravel, WordPress, and custom PHP apps.

SEO Keywords

  • NGINX PHP MySQL Amazon Linux 2023
  • LEMP stack AWS setup
  • Install NGINX Amazon Linux 2023
  • PHP MySQL EC2 setup
  • AWS web server configuration

Clean Code in Large Projects — Patterns I Use as a Full-Stack Developer

Clean Code in Large Projects — Patterns I Use as a Full-Stack Developer

Clean code isn’t about writing “beautiful” code. It’s about writing code that teams can understand, extend, and debug without fear.

In small side projects, you can often get away with messy code. But in large applications with multiple developers, clean code stops being a preference and becomes a survival strategy. In this post, I’ll share the principles and patterns I rely on as a full-stack developer to keep large codebases manageable.

1. Single Responsibility Everywhere

The Single Responsibility Principle (SRP) is simple: every function, class, or module should do one thing and do it well.

For example, instead of one giant function that validates input, saves to the database, sends an email, and logs activity, break it into smaller pieces:

  • A validator
  • A repository or data access function
  • A notification or email service
  • A logger

This makes testing easier and reduces side effects when you change one part.

2. Use Descriptive Names Instead of Comments

Good naming is one of the most underrated skills in software development. If your variables and functions are well named, you’ll need fewer comments.

// bad
let x = get(u);

// good
let activeUser = getActiveUserById(userId);

When you read code months later, descriptive names act like documentation.

3. Avoid Deep Nesting

Long chains of if/else blocks or nested callbacks quickly become hard to follow. Prefer early returns and guard clauses:

// bad
function handle(user) {
  if (user) {
    if (user.isActive) {
      // logic
    } else {
      // ...
    }
  } else {
    // ...
  }
}

// better
function handle(user) {
  if (!user) return;
  if (!user.isActive) return;
  // main logic
}

Flattening control flow improves readability and reduces cognitive load.

4. Organize Code by Feature, Not by File Type

In large applications, grouping files only by type (controllers, models, services) can make it hard to see the whole picture of a feature.

I prefer a feature-based structure:

/users
  controllers/
  services/
  models/
  routes/

 /orders
  controllers/
  services/
  models/
  routes/

This way, when you work on “orders”, everything related to orders is in one place.

5. Encapsulate Business Logic

Business rules should not live inside controllers, route handlers, or UI components. Those layers should coordinate, not decide.

Instead, put business logic inside services or use-case classes. For example:

  • CreateOrderService
  • UpdateProfileService
  • CalculateInvoiceService

This keeps controllers thin and makes business logic reusable (e.g., from a CLI, queue worker, or scheduled job).

6. DRY, But Not Over-Abstracted

“Don’t Repeat Yourself” is important, but over-abstracting code is just as dangerous as duplication. If you extract a piece of logic prematurely, you might end up with a generic function that’s hard to understand and easy to misuse.

A good rule: abstract only when you see the same pattern at least two or three times and you’re confident the abstraction makes the code clearer, not more confusing.

7. Enforce Standards with Linters and Formatters

Manual code style reviews waste time and energy. Let tools handle that:

  • Prettier for formatting JavaScript/TypeScript
  • ESLint for code quality rules
  • PHPCS or Laravel Pint for PHP projects

Run these tools automatically on commit or in CI. This keeps the codebase consistent and reduces “style debates” in pull requests.

Patterns I Use Frequently

Pattern Why I Use It
MVC / Modular Architectures Separates concerns (UI, logic, data access) and keeps each layer focused.
Repository Pattern Abstracts database operations and makes it easier to change underlying storage.
Dependency Injection Makes modules easier to test and swap (e.g., different implementations per environment).
Factory Pattern Centralizes complex object creation and reduces duplication.
Adapter Pattern Wraps external APIs or services so the rest of the codebase doesn’t depend on their details.

Final Thoughts

Clean code is not about impressing other developers. It’s about making your future work easier.

When your codebase is organized, predictable, and readable, you can:

  • Ship features faster
  • Onboard new developers smoothly
  • Reduce bugs introduced by “mysterious” side effects

If you’re working on a large project and feel the codebase is getting out of control, I’m always open to discussing architecture and refactoring strategies. You can find me at NAVANEETH.ME.

How to Build Lightning-Fast Web Apps: Database Optimization Techniques for MySQL & MongoDB

How to Build Lightning-Fast Web Apps: Database Optimization Techniques for MySQL & MongoDB

Most “slow website” complaints are not actually frontend problems. In many real-world projects, the bottleneck lives in the database layer.

Whether you’re using MySQL or MongoDB, understanding how to model data and query it efficiently is one of the best ways to make your web apps feel instantly faster. In this post, I’ll share practical database optimization techniques I use in production for both MySQL and MongoDB.

Optimizing MySQL for Performance

1. Index the Right Columns

Indexes are critical for speeding up read queries. Focus on columns that appear in:

  • WHERE clauses
  • JOIN conditions
  • ORDER BY and GROUP BY clauses

However, adding too many indexes can slow down writes. The key is balance — index what you actually query often.

2. Avoid SELECT *

SELECT * is convenient, but it’s a performance killer on large tables. Fetching unnecessary columns increases I/O and network usage.

Instead, request only what you need:

SELECT id, name, price FROM products WHERE id = 123;

3. Normalize First, Denormalize When Necessary

Start with a normalized schema to avoid duplicate and inconsistent data. Over time, when you identify performance hotspots, you can selectively denormalize to reduce joins.

Examples of safe denormalization:

  • Storing a user’s name and email snapshot on an order record
  • Caching frequently used aggregates in separate tables

4. Choose Proper Data Types

Using the right data type can have a big impact. For example:

  • Use INT instead of VARCHAR for IDs
  • Use TINYINT for boolean-like flags
  • Use DATETIME or TIMESTAMP for dates instead of strings

5. Use EXPLAIN to Debug Slow Queries

Whenever you notice a slow query, run it with EXPLAIN. You’ll see how MySQL is executing it and whether it’s using indexes properly.

EXPLAIN SELECT id, name FROM users WHERE email = 'test@example.com';

This helps you decide whether you need a new index, a different query pattern, or a change in schema.

Optimizing MongoDB for Performance

1. Design Schema Around Access Patterns

MongoDB is document-based, which gives you more flexibility. But that also means schema design should start from how your application reads data.

General rule of thumb:

  • Embed data when it is read together most of the time
  • Reference data when it changes independently or is shared widely

2. Use Indexes Strategically

Just like in MySQL, indexes in MongoDB are essential. Focus on:

  • Fields used in find() queries
  • Fields used in sort operations
  • Compound indexes for common multi-field queries

An example of a compound index:

db.orders.createIndex({ userId: 1, createdAt: -1 });

3. Be Careful with Large, Unbounded Arrays

Storing huge arrays inside a single document can cause performance and indexing issues. If an array grows without limit, consider modeling it differently:

  • Move items into a separate collection
  • Use pagination at the collection level instead of a giant embedded array

4. Optimize Aggregation Pipelines

When using aggregation pipelines, filter and project as early as possible.

For example:

db.orders.aggregate([
  { $match: { status: "PAID" } },
  { $project: { items: 1, total: 1, userId: 1 } },
  { $sort: { createdAt: -1 } },
  { $limit: 20 }
]);

This keeps the pipeline efficient and avoids carrying unnecessary fields through each stage.

5. Monitor with Profiling and APM Tools

MongoDB offers built-in profiling. Cloud providers also give dashboard insights. Slow queries often show up there long before users start complaining.

Universal Performance Techniques

Regardless of whether you’re using MySQL, MongoDB, or both, some optimization strategies are universal:

Area Technique
Caching Use Redis, in-memory cache, or HTTP caching for frequently accessed data.
Connection Management Reuse database connections with pooling instead of creating a new connection per request.
Pagination Always paginate large result sets to avoid loading thousands of rows/documents at once.
Background Jobs Move heavy operations (reports, exports, emails) to background queues.
Read Replicas Offload read-heavy traffic to replicas when scaling.

Final Thoughts

Fast web apps don’t happen by accident. They are the result of conscious decisions in data modeling, indexing, and query design.

If you treat the database as a core part of your application architecture, not just a storage layer, you’ll deliver snappier experiences and scale with much less pain.

If you’re planning to optimize or redesign your application’s database layer and want a second opinion, you can always reach out to me through NAVANEETH.ME.

API Design Best Practices That Reduce Future Tech Debt

API Design Best Practices That Reduce Future Tech Debt

Building an API is easy. Building an API that still makes sense three years later is the real challenge.

As a full-stack developer and CTO, I’ve seen how poor API design slowly turns into tech debt, broken integrations, and painful migrations. In this post, I’ll share practical API design principles that help you avoid that pain and keep your system flexible as it grows.

1. Think in Resources, Not Actions

A common early mistake is designing APIs like RPC calls:

  • /getUserDetails
  • /updateUser
  • /deleteUserById

This quickly becomes messy. A better approach is to think in terms of resources and use HTTP verbs properly:

GET    /users
GET    /users/:id
POST   /users
PUT    /users/:id
DELETE /users/:id

Clients can now rely on consistent patterns, and you avoid a swarm of custom endpoints that all do similar things.

2. Version Your API from Day One

APIs evolve. Fields get renamed, behaviors change, and sometimes you need to break compatibility. If you don’t version your API, every change becomes risky.

Use clear versioning in your URL or headers, for example:

/api/v1/users
/api/v2/users

This allows you to ship improvements without immediately breaking existing clients. You can phase out old versions gradually instead of forcing a big-bang migration.

3. Keep Your Responses Predictable

Clients should never have to guess what they’ll get back from an endpoint. Avoid returning different shapes for the same resource.

Define a consistent schema and stick to it. For example, a typical error response might look like:

{
  "status": "error",
  "code": "USER_NOT_FOUND",
  "message": "User does not exist"
}

Whether it’s authentication, validation, or business logic errors — the outer structure should remain consistent.

4. Return Only What Clients Need

Over time, it becomes tempting to keep adding more and more fields to an API response. That leads to slow responses and wasted bandwidth.

Instead, support filtering and field selection:

GET /products?fields=id,name,price
GET /users?role=admin&page=1&limit=50

This keeps your API flexible and lets different clients (web, mobile, third-party) fetch only what they need.

5. Use Pagination for Collections

Never return thousands of records in a single response. It’s bad for performance, bad for memory, and bad for user experience.

Use a standard pagination strategy:

GET /users?page=1&limit=20

Or cursor-based pagination for very large datasets:

GET /users?cursor=eyJpZCI6MTIzfQ==&limit=20

6. Design for Idempotency

Network issues happen. Clients might retry requests. Your API should be able to handle repeated calls safely, especially for operations like payments, cancellations, and updates.

For example, using PUT to update a resource is naturally idempotent:

PUT /orders/:id/cancel

Even if the client sends the request twice, the order will still be in the “cancelled” state, not “cancelled twice”.

7. Use Proper HTTP Status Codes

HTTP status codes are there to communicate what happened. Use them consistently:

  • 200 – OK
  • 201 – Created
  • 400 – Bad Request (validation errors)
  • 401 – Unauthorized (no or invalid token)
  • 403 – Forbidden (no permission)
  • 404 – Not Found
  • 409 – Conflict (duplicate data, business rules)
  • 500 – Internal Server Error

8. Document Your API as If You’re a Third-Party Developer

Good documentation is part of API design. Tools like OpenAPI/Swagger, Postman Collections, or auto-generated docs make it easier for others (including your future self) to consume the API.

As a rule, if a new developer can understand your API in under an hour from the docs alone, you’re doing it right.

API Smells That Predict Future Tech Debt

Smell Better Approach
Endpoints named like functions (/doSomething) Resource-based URLs (/users/:id)
Mixing unrelated concerns in one endpoint Small, focused endpoints per responsibility
No versioning Clear versioned API (/api/v1)
Inconsistent error shapes Standard error schema for all failures

Final Thoughts

API design is not just about getting data from point A to point B. It’s about creating a contract that’s easy to extend, safe to evolve, and pleasant to work with.

If you design your APIs with versioning, predictability, and clear structure from the beginning, you’ll save yourself and your team from a lot of refactoring and firefighting later.

If your team is struggling with a messy API layer and you’re considering a redesign, feel free to reach out via NAVANEETH.ME — I’d be happy to share ideas and approaches.