Ufraan's Notes Digital garden & personal knowledge base
Last modified: Jun 28, 2026Home / 00_inbox / Backend Developer Roadmap.Md

Backend Developer Roadmap: From Zero to Production Ready

Table of Contents

Introduction

So you want to become a backend developer. Good choice. The backend is where the real magic happens. It is the brain behind every app you use. When you open Instagram and see your feed, when you order food on Swiggy, when you search for something on Google, the backend is what makes it all work. The frontend is just the pretty face. The backend is the heart and the guts.

This guide is going to take you from absolute zero all the way to a level where you can build production ready backend systems. I am not going to just throw a bunch of technologies at you. I am going to explain the why behind everything. Why do we need databases. Why do we need APIs. Why do we need containers. Understanding the why is what separates a real developer from someone who just follows tutorials.

There are no shortcuts here. This roadmap is designed to take time. You cannot become a backend developer in a week or a month. Anyone who promises you that is lying. But if you stick with it and build the projects I suggest, you will get there.

One more thing before we start. This guide is written in plain casual language. I am not going to use fancy jargon to sound smart. I am going to explain things the way I would explain them to a friend. If something is confusing, read it again. If it is still confusing, go build something and come back. Things click when you build.

The Tech Stack We Will Focus On

This roadmap centers around three languages: Go, Python, and TypeScript. Why these three?

Go is modern, fast, and incredibly good at handling concurrent requests. It is used by companies like Google, Uber, Dropbox, and Twitch for their backend infrastructure. Go compiles to a single binary, deploys easily, and has a standard library that includes an HTTP server out of the box. You do not need a framework for basic things in Go.

Python is the most beginner friendly language that is also used extensively in production. Companies like Instagram, Spotify, and Netflix use Python in their backend stacks. Python has amazing frameworks like FastAPI and Django. It is also the dominant language for data science, machine learning, and AI. Knowing Python opens many doors.

TypeScript is JavaScript with types. If you want to do full stack development, TypeScript lets you use one language for frontend and backend. Node.js with TypeScript is extremely popular for building APIs. Companies like LinkedIn, Netflix, and Trello use Node.js in production.

We will also touch on Java. Java is everywhere in enterprise backend development. Banks, insurance companies, large corporations all run Java. If you want to be employable in the traditional enterprise sector, Java is essential. We will include a couple of Java projects for exactly this reason.

Learning three languages plus Java might sound like a lot. But here is the truth. Once you learn your first language well, the second one takes half the time. The third takes a quarter of the time. By the time you know three languages, you can pick up a new one in a week. The concepts transfer. Only the syntax changes.

Alright. Let us begin.

Who Is This For

This guide is for absolute beginners. If you have never written a line of code in your life, you are in the right place. If you have done some frontend work and want to move to the backend, you are also in the right place. If you are a CS student who feels like your college taught you nothing practical, yes this is for you too.

The only thing I expect from you is patience and a willingness to build things. If you have that, everything else can be learned.

This guide is also for you if you already know one language and want to expand your toolkit. Maybe you know Python and want to learn Go. Or you know JavaScript and want to add Python to your resume. This roadmap will show you how all the pieces fit together regardless of which language you start with.

Phase 0: The Right Mindset

Before we write a single line of code, we need to talk about mindset. This is the most important section in this entire guide. Read it carefully.

Learning How to Learn

Most beginners make the same mistake. They try to learn everything at once. They watch a tutorial on Python, then jump to a tutorial on Docker, then watch something about Kubernetes, and end up more confused than when they started. This is called tutorial hell and it is the number one reason people give up.

The right way to learn is to pick one thing and go deep. Build something with it. Get comfortable. Then add the next thing. Layer by layer.

Think of it like building a house. You do not start with the roof. You start with the foundation. In backend development, the foundation is understanding how computers work, how the internet works, and how to write basic programs. Everything else builds on top of that.

Here is a concrete example. If you are learning Python, do not watch a five hour tutorial on Python and then move on. Write a hundred small programs in Python. Build a calculator. Build a todo list. Build a file organizer. Get bored with Python. Then you are ready to learn something new.

Embrace Being a Beginner

You are going to suck at first. Everyone does. The best engineers I know wrote terrible code when they started. The difference between them and people who gave up is that they kept going.

When you hit a bug you cannot solve, and you will hit many of them, do not get frustrated. Get curious. Bugs are puzzles. Every bug you solve teaches you something. The more bugs you solve, the better you get.

Here is a mindset shift that helps. Instead of thinking “I am stuck on this bug,” think “I am about to learn something new.” The bug is not an obstacle. It is a lesson. The only way to lose is to give up before you find the answer.

Build Things

You cannot learn backend development by reading. You cannot learn it by watching videos. You can only learn it by building. Reading and watching help, but they are not the main event.

Every time you learn a new concept, build something with it. Even if it is tiny. Even if it is ugly. Even if nobody will ever see it. Build it.

The projects I have included in this guide are not optional. They are the most important part of this roadmap. If you skip the projects and just read the text, you will not become a backend developer. You will become someone who has read about backend development. There is a difference.

Use Google Like a Pro

Real developers Google things constantly. I have been doing this for years and I still search for basic syntax multiple times a day. The skill is not knowing everything. The skill is knowing how to find what you need.

When you get an error, copy the error message and paste it into Google. Read the first few results. Do not just copy paste the fix. Understand why the error happened. That understanding is what makes you a better developer.

Also learn to use AI tools like ChatGPT and Claude. They are amazing for explaining concepts, debugging, and generating boilerplate. But do not let them do your thinking for you. Use them as a tutor, not a replacement for your brain.

A good way to use AI is to ask it to explain a concept and then ask follow up questions. Why does this work this way? What happens if I change this? What are the alternatives? Treat AI like a senior developer who is patiently answering all your questions.

Consistency Over Intensity

Studying for 10 hours on a Saturday and then doing nothing for a week is not effective. Studying for 1 hour every day is much better. Your brain learns best when you expose it to something regularly.

Try to code every day. Even if it is just 30 minutes. Even if you just fix one small bug or write one small function. The consistency matters more than the intensity.

Here is a trick. Set a timer for 25 minutes and code for that long. That is called the Pomodoro technique. Anyone can code for 25 minutes. Usually after 25 minutes you will want to keep going. But even if you stop, you have made progress.

The 80/20 Rule

In backend development, 20 percent of the concepts cover 80 percent of what you need on a daily basis. You do not need to know everything. You need to know the core concepts deeply.

What are the core concepts? How to write code. How to use version control. How to build an API. How to work with a database. How to deploy. That covers most of what a backend developer does every day.

Do not get distracted by shiny new tools. Every week there is a new framework or a new database. Ignore them. Master the fundamentals. The tools change. The fundamentals do not.

Alright. Mindset is sorted. Now let us actually learn some things.

Phase 1: Computer Science Fundamentals

You cannot build backend systems without understanding the fundamentals. This is the foundation layer. Skip it at your own risk.

How Computers Work

At the most basic level, a computer is a machine that processes instructions. It has a CPU that does the thinking, RAM that holds short term memory, and a hard drive or SSD that holds long term memory.

When you write code, the CPU reads your instructions one by one and executes them. The RAM holds the data your program is currently working with. The hard drive stores files and data that need to persist even when the computer is turned off.

This matters for backend development because backend systems deal with millions of requests. You need to understand that reading from RAM is fast. Reading from a hard drive is slow. This is why we use caches like Redis. More on that later.

Learn about these concepts in detail: - CPU, RAM, Storage. What does each one do? Why is RAM faster than disk? What happens when you run out of RAM? - How programs are loaded into memory and executed. What is the difference between compiled and interpreted languages? Go is compiled. Python is interpreted. What does that actually mean? - Processes and threads. What is the difference? How does an operating system manage multiple programs running at the same time? Why does this matter for a web server that handles many requests? - The difference between sequential and parallel execution. Why can a Go server handle thousands of connections while a Python server might struggle? The answer involves threads, the GIL in Python, and goroutines in Go.

You do not need a computer science degree to understand these things. Spend a weekend reading about them. Watch some YouTube videos. The understanding will serve you for your entire career.

Operating Systems Basics

As a backend developer, you will likely deploy your code on Linux servers. Most of the internet runs on Linux. So you need to be comfortable with it.

Start by understanding: - What an operating system does. It manages hardware, runs programs, handles files, and provides a way for programs to interact with the computer. - File systems and permissions. Linux has a hierarchical file system starting from root. Everything is a file. Permissions control who can read, write, and execute. - Processes and how they are managed. Every running program is a process. The OS scheduler decides which processes get CPU time. - The terminal and command line. This is how you interact with a Linux server. There is no mouse. Everything is text commands.

Spend time learning basic Linux commands. Learn how to navigate the file system, how to read files, how to search for things, how to manage processes. These skills will be invaluable when you start deploying your applications.

Commands you should know by heart:

File operations: - ls: List files in a directory. ls -la shows all files including hidden ones with details. - cd: Change directory. cd .. goes up one level. cd ~ goes home. - pwd: Print working directory. Tells you where you are. - mkdir: Create a directory. mkdir -p creates parent directories too. - rm: Remove files. rm -rf removes directories recursively. Be careful with this one. - cp: Copy files. cp -r copies directories. - mv: Move or rename files.

Reading files: - cat: Print the entire file to terminal. - less: View a file page by page. Press space to go forward, q to quit. - head: Show the first 10 lines of a file. head -n 50 shows 50 lines. - tail: Show the last 10 lines. tail -f follows the file as it grows. Great for logs.

Searching: - grep: Search for text in files. grep “error” log.txt finds lines containing error. - find: Find files by name. find . -name “*.py” finds all Python files.

Process management: - ps: Show running processes. ps aux shows all processes. - top: Interactive process viewer. Shows CPU and memory usage. - kill: Stop a process. kill -9 forces it to stop.

System: - chmod: Change file permissions. chmod +x script.py makes it executable. - chown: Change file owner. - ssh: Connect to a remote server. ssh user@hostname. - nano or vim: Text editors in the terminal. Learn the basics of at least one.

You do not need to become a Linux expert overnight. But you should be comfortable in a terminal. Practice by doing all your file operations in the terminal for a week. It will feel slow at first. It gets faster.

Networking Basics

The backend is all about communication between computers. So you need to understand how computers talk to each other.

Start with these concepts:

IP addresses and ports. Every computer on the internet has an IP address. It is like a phone number for your computer. Ports are like different phone lines on the same computer. Port 80 is for HTTP. Port 443 is for HTTPS. Port 5432 is for PostgreSQL. When you connect to a server, you need both the IP address and the port.

TCP and UDP. These are protocols for sending data over the internet. TCP is reliable. It guarantees that data arrives in order and nothing is lost. This is what web traffic uses. UDP is faster but unreliable. It is used for video streaming and online gaming where speed matters more than perfect delivery.

The request response cycle. When you type a URL into your browser, here is exactly what happens. Your browser asks a DNS server for the IP address of the domain. DNS responds with the IP. Your browser opens a TCP connection to that IP on port 443. It sends an HTTP request. The server processes the request and sends back an HTTP response. Your browser renders the response as a web page.

DNS. The Domain Name System translates human readable names like google.com into IP addresses. It is like a phone book for the internet. When you type a URL, your computer asks a DNS server. If the DNS server does not know, it asks another DNS server. This continues up the chain until someone knows the answer.

HTTP methods. GET retrieves data. POST creates new data. PUT updates existing data. DELETE removes data. PATCH partially updates data. Each method has a specific meaning and specific rules about caching, safety, and idempotency.

HTTP status codes. 200 means success. 201 means created. 301 means moved permanently. 400 means bad request (client error). 401 means unauthorized. 403 means forbidden. 404 means not found. 500 means internal server error (server error). Understanding these codes helps you debug issues quickly.

This is the most fundamental concept in web development. Understand this deeply before moving on.

Data Structures and Algorithms

I know. Everyone says this. It sounds boring. But here is the truth. You do not need to be a DSA wizard to be a backend developer. You do need to understand the basics.

Learn these data structures:

Arrays and lists. A collection of items stored in order. Accessing an element by index is fast. Inserting or deleting in the middle is slow because everything needs to shift.

Hash tables. Called dictionaries in Python, maps in Go, objects in JavaScript. They store key value pairs. Looking up a value by key is extremely fast, almost instant regardless of how many items are in the table. This is the most useful data structure in backend development.

Stacks and queues. A stack is last in first out. Like a stack of plates. A queue is first in first out. Like a line at a store. These are used everywhere in backend systems for managing tasks and requests.

Trees. A hierarchical structure with nodes and branches. Databases use trees (B trees) internally for indexes. Understanding trees helps you understand how database indexes work.

Big O notation. This describes how your code performs as the input size grows. O(1) means constant time. It takes the same amount of time regardless of input size. O(n) means linear time. It grows proportionally with input size. O(n squared) means quadratic time. It gets slow very quickly.

When you have a million users and each user has a thousand posts, and you write code that loops through every post of every user, your code will be slow. Understanding Big O helps you avoid this. You will look at your code and say “this is O(n squared), I need a hash map to make it O(n).”

You do not need to solve LeetCode hard problems. But you should understand when to use a hash map versus an array. You should understand why searching in a list is slow for large data. You should understand why database indexes use tree structures.

Phase 2: Pick Your First Language and Master It

You need to pick one language and get good at it. Do not try to learn multiple languages at the same time. Pick one, build projects with it, get comfortable, and then you can learn others easily.

The path I recommend for this roadmap is Python first. Python is the easiest to learn. Then Go. Then TypeScript. Then Java for enterprise employability.

But if you already know one of these languages, start with that one and then learn the others in the order above.

Python: Your First Language

If you are a complete beginner, start with Python. It is the most beginner friendly language. The syntax is clean and reads almost like English. You can build real things quickly without getting bogged down by complex syntax.

Here is what Python code looks like:

def greet(name):
    return f"Hello, {name}!"

print(greet("World"))

See how clean that is? No curly braces. No semicolons. Just English.

Python is used extensively in backend development. Django is a full featured framework that includes everything you need to build a web application. FastAPI is a modern framework for building APIs that is fast and has great documentation.

Python is also the dominant language in data science and machine learning. Even if you go into backend development, knowing Python is a huge asset. Many backend systems have data processing components that are best written in Python.

What to learn in Python in order:

Variables and data types:

name = "John"        # String
age = 30             # Integer
price = 19.99        # Float
is_active = True     # Boolean

Lists, dictionaries, sets, tuples:

fruits = ["apple", "banana", "cherry"]     # List
person = {"name": "John", "age": 30}        # Dictionary
unique = {1, 2, 3}                          # Set
coordinates = (10, 20)                      # Tuple

Control flow:

if age >= 18:
    print("Adult")
elif age >= 13:
    print("Teen")
else:
    print("Child")

for fruit in fruits:
    print(fruit)

while count > 0:
    print(count)
    count -= 1

Functions:

def add(a, b):
    return a + b

# Default arguments
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

Classes and objects:

class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email

    def send_email(self, message):
        print(f"Sending to {self.email}: {message}")

user = User("John", "john@example.com")
user.send_email("Welcome!")

File handling:

# Reading
with open("file.txt", "r") as f:
    content = f.read()

# Writing
with open("file.txt", "w") as f:
    f.write("Hello, World!")

Error handling:

try:
    result = 10 / 0
except ZeroDivisionError:
    print("Cannot divide by zero")
except Exception as e:
    print(f"Something else went wrong: {e}")
finally:
    print("This always runs")

Modules and packages:

# Import from standard library
import os
import json
from datetime import datetime

# Import third party
import requests

Virtual environments and pip:

python -m venv venv
source venv/bin/activate
pip install requests
pip freeze > requirements.txt

Build these projects to learn Python: - A calculator that takes user input and performs basic math operations - A todo list manager that runs in the terminal with add, list, complete, and delete commands - A program that reads a text file and reports the most common words - A number guessing game where the computer picks a number and you guess it - A CSV file parser that reads a CSV and prints formatted data

Each project teaches specific skills. The calculator teaches functions and control flow. The todo list teaches data structures and user input. The word counter teaches file handling and string manipulation. The guessing game teaches loops and conditionals. The CSV parser teaches file I/O and data parsing.

Build all five before moving on.

Go: Your Second Language

After Python, learn Go. Go is a completely different experience. It is compiled, statically typed, and built for performance. Learning Go after Python will make you a better programmer because Go forces you to think about things Python hides.

Go was created at Google by some of the same people who created C. They wanted a language that was simple, fast, and good at handling concurrent operations. Go compiles to a single binary. You can write a web server in Go without any frameworks.

Here is what Go code looks like:

package main

import "fmt"

func greet(name string) string {
    return fmt.Sprintf("Hello, %s!", name)
}

func main() {
    fmt.Println(greet("World"))
}

Notice the differences from Python. Types are explicit. The function signature declares what type it returns. The opening brace is on the same line as the function declaration. These are not just stylistic choices. They are enforced by the compiler.

What to learn in Go in order:

Basic syntax and types:

var name string = "John"
age := 30                     // Short declaration
var price float64 = 19.99
isActive := true

// Multiple return values
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("cannot divide by zero")
    }
    return a / b, nil
}

Structs and interfaces:

type User struct {
    Name  string
    Email string
    Age   int
}

func (u User) SendEmail(message string) {
    fmt.Printf("Sending to %s: %s\n", u.Email, message)
}

// Interfaces define behavior, not data
type Notifier interface {
    Notify(message string) error
}

Arrays and slices:

// Arrays have fixed size
var nums [5]int

// Slices are dynamic
fruits := []string{"apple", "banana", "cherry"}
fruits = append(fruits, "date")

Maps:

user := map[string]string{
    "name":  "John",
    "email": "john@example.com",
}

// Check if key exists
if value, exists := user["phone"]; exists {
    fmt.Println(value)
}

Goroutines and channels. This is what makes Go special:

// Start a goroutine (lightweight thread)
go func() {
    fmt.Println("Running in background")
}()

// Channels for communication
ch := make(chan string)
go func() {
    ch <- "Hello from goroutine"
}()
message := <-ch
fmt.Println(message)

HTTP server with standard library:

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
)

type Post struct {
    Title   string `json:"title"`
    Content string `json:"content"`
}

func handlePosts(w http.ResponseWriter, r *http.Request) {
    if r.Method == "GET" {
        posts := []Post{
            {Title: "First Post", Content: "Hello World"},
        }
        json.NewEncoder(w).Encode(posts)
    } else if r.Method == "POST" {
        var post Post
        json.NewDecoder(r.Body).Decode(&post)
        fmt.Println("Received post:", post.Title)
        json.NewEncoder(w).Encode(post)
    }
}

func main() {
    http.HandleFunc("/posts", handlePosts)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Go forces good habits. It does not let you compile with unused variables. It does not let you import packages you do not use. It has a built in formatter (gofmt) that everyone uses so all Go code looks the same. The standard library is comprehensive. You can build a production web server with just the standard library.

Build these projects in Go: - A command line tool that fetches weather data from an API and prints it - A REST API server for managing tasks (CRUD with in memory storage) - A concurrent web scraper that fetches multiple URLs in parallel using goroutines - A file server that serves static files from a directory

TypeScript: Your Third Language

TypeScript is JavaScript with types. If you know JavaScript, TypeScript is the logical next step. If you are learning from scratch, learn TypeScript directly rather than plain JavaScript.

TypeScript catches errors at compile time that JavaScript would only catch at runtime. This is huge for backend development where you want reliability.

Here is what TypeScript code looks like:

function greet(name: string): string {
    return `Hello, ${name}!`;
}

console.log(greet("World"));

What to learn in TypeScript:

Basic types:

let name: string = "John";
let age: number = 30;
let isActive: boolean = true;
let items: string[] = ["a", "b", "c"];
let person: { name: string; age: number } = { name: "John", age: 30 };

Interfaces and types:

interface User {
    id: number;
    name: string;
    email: string;
    age?: number;  // Optional
}

type Status = "active" | "inactive" | "banned";

function createUser(data: User): void {
    console.log(`Creating user: ${data.name}`);
}

Async/await:

async function fetchUser(id: number): Promise<User> {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) {
        throw new Error("Failed to fetch user");
    }
    return response.json();
}

Express with TypeScript:

import express, { Request, Response } from "express";

const app = express();
app.use(express.json());

interface Post {
    id: number;
    title: string;
    content: string;
}

app.get("/posts", (req: Request, res: Response) => {
    const posts: Post[] = [
        { id: 1, title: "First Post", content: "Hello" }
    ];
    res.json(posts);
});

app.listen(3000, () => {
    console.log("Server running on port 3000");
});

Build these projects in TypeScript: - A REST API with Express and TypeScript - A CLI tool that reads and writes JSON files - An API that connects to PostgreSQL using Prisma ORM

Java: For Enterprise Employability

Java is the old guard. It has been around for decades and it is not going anywhere. Banks, insurance companies, government systems, large enterprises all run Java. If you want job security and high paying enterprise roles, learn Java.

Java is verbose compared to Go and Python. But it is powerful and has an enormous ecosystem.

Here is what Java code looks like:

public class Main {
    public static void main(String[] args) {
        System.out.println(greet("World"));
    }

    public static String greet(String name) {
        return "Hello, " + name + "!";
    }
}

What to learn in Java:

Basic syntax:

String name = "John";
int age = 30;
double price = 19.99;
boolean isActive = true;
String[] fruits = {"apple", "banana", "cherry"};

Classes and objects:

public class User {
    private String name;
    private String email;

    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public String getName() {
        return name;
    }

    public void sendEmail(String message) {
        System.out.println("Sending to " + email + ": " + message);
    }
}

Spring Boot is the standard framework for building Java backends:

@RestController
@RequestMapping("/api/posts")
public class PostController {

    @GetMapping
    public List<Post> getPosts() {
        return List.of(
            new Post(1, "First Post", "Hello World")
        );
    }

    @PostMapping
    public Post createPost(@RequestBody Post post) {
        System.out.println("Creating post: " + post.getTitle());
        return post;
    }
}

Build these projects in Java: - A REST API with Spring Boot for managing books in a library - A simple banking application with account creation, deposits, and withdrawals

Phase 3: Version Control with Git

If you only learn one tool besides your programming languages, make it Git. Git is how developers track changes to their code, collaborate with others, and manage different versions of their projects.

Why Git

Imagine you are working on a project and you want to try something new. You are not sure if it will work. Without Git, you would copy your entire project folder and work on the copy. Then when things get messy, you have folders everywhere. project_v1, project_v2, project_final, project_final_actual.

Git solves this. You create branches to try new things. If it works, you merge it. If it does not, you delete the branch. Your main code stays clean.

Git also lets you go back in time. If you break something, you can revert to a previous version. It is like a time machine for your code.

Git is also how teams collaborate. Multiple developers can work on the same codebase without stepping on each others toes. Git tracks who changed what and when.

Basic Concepts

Repository: A folder tracked by Git. It contains your project files plus a hidden .git folder that stores all the history.

Commit: A snapshot of your code at a point in time. Each commit has a unique ID, a timestamp, an author, and a message describing what changed.

Branch: A separate line of development. The main branch is usually called main or master. You create feature branches to work on new features without affecting the main code.

Remote: A copy of your repository on another server. GitHub is the most popular remote hosting service.

Push: Send your commits from your local machine to the remote repository.

Pull: Download commits from the remote repository to your local machine.

Merge: Combine changes from one branch into another.

Commands You Need to Know

# Start tracking a folder
git init

# Check status of your files
git status

# Stage files for commit
git add filename.py
git add .  # Stage all changed files

# Commit staged changes
git commit -m "Add user authentication feature"

# View commit history
git log
git log --oneline  # Compact view

# Create and switch branches
git branch feature-login
git checkout feature-login

# Shortcut: create and switch
git checkout -b feature-login

# Merge a branch into current branch
git checkout main
git merge feature-login

# Connect to remote
git remote add origin https://github.com/username/repo.git

# Push to remote
git push -u origin main  # First time
git push                 # Subsequent times

# Pull from remote
git pull

# Clone a repository
git clone https://github.com/username/repo.git

# See what changed in a file
git diff

# Undo staging
git reset HEAD filename

# Discard changes
git checkout -- filename

Essential Workflow

Here is the standard workflow you will use every day:

# Start your day
git checkout main
git pull

# Create a branch for your feature
git checkout -b feature-add-search

# Make changes, then
git add .
git commit -m "Add search functionality"

# Push to remote
git push -u origin feature-add-search

# On GitHub, create a pull request
# Team reviews your code
# Merge to main

# Back on your machine
git checkout main
git pull

.gitignore

Some files should never be committed. Virtual environments, node_modules, compiled binaries, environment variable files, and IDE settings.

Create a .gitignore file in your repository root:

venv/
node_modules/
.env
*.pyc
__pycache__/
.DS_Store
dist/
build/
*.exe

GitHub

GitHub is where developers store their code and collaborate. Create an account. Push your projects there. It serves as your portfolio when applying for jobs.

Your GitHub profile is your resume. Employers look at it. Keep it clean. Have a profile picture. Write a bio. Pin your best projects. Write good README files.

A good README includes: - Project name and description - Technologies used - How to install and run - API documentation (if applicable) - Screenshots (if applicable) - License

What to Do

Initialize a Git repository for every project you build from now on. Make frequent commits with meaningful messages. Push your projects to GitHub. This builds the habit and gives you a portfolio.

Here is a rule. Commit after every logical change. Not after every line. Not after 500 lines. After each complete unit of work. Added a function? Commit. Fixed a bug? Commit. Updated the README? Commit.

Good commit messages are imperative and specific: - Bad: “fixed stuff” - Good: “Fix crash when user submits empty form” - Bad: “updates” - Good: “Add email validation to registration endpoint”

Phase 4: Understanding the Web

Now we get into the actual backend stuff. You need to understand how the web works before you can build for it.

The Full Picture

When you type a URL into your browser and press Enter, a whole chain of events happens in milliseconds. Understanding this chain is essential for every backend developer.

Step 1: Your browser parses the URL. It identifies the protocol (https), the domain (example.com), and the path (/api/users).

Step 2: Your browser checks its cache for the IP address of the domain. If not found, it asks the operating system. If the OS does not know, it asks a DNS resolver.

Step 3: The DNS resolver looks up the domain. If it is not in its cache, it asks the root DNS server, which directs it to the TLD server (.com), which directs it to the authoritative nameserver for the domain. The authoritative server returns the IP address.

Step 4: Your browser opens a TCP connection to that IP address on port 443 (HTTPS). TLS handshake happens to establish an encrypted connection.

Step 5: Your browser sends an HTTP request. The request includes the method (GET), the path (/api/users), headers (accept types, cookies, user agent), and optionally a body.

Step 6: The server receives the request. It parses the headers. It routes the request to the appropriate handler based on the path and method.

Step 7: The handler processes the request. It might query a database, call another service, or compute something. It prepares a response.

Step 8: The server sends back an HTTP response with a status code, headers, and a body (usually JSON).

Step 9: Your browser receives the response. It parses the JSON. It renders the page or updates the UI.

Every backend developer should understand each of these steps. When something breaks, you need to know which step to debug.

HTTP Deep Dive

HTTP is a text based protocol. You can actually read HTTP requests and responses. Here is what they look like.

An HTTP request:

GET /api/users HTTP/1.1
Host: example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Accept: application/json
User-Agent: Mozilla/5.0

An HTTP response:

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 42
Date: Mon, 01 Jan 2024 00:00:00 GMT

{"users":[{"id":1,"name":"John"}]}

Status codes you must know:

2xx Success: - 200 OK: The request succeeded. - 201 Created: A new resource was created. Used with POST. - 204 No Content: The request succeeded but there is no body to return. Used with DELETE.

3xx Redirection: - 301 Moved Permanently: The resource has moved to a new URL. Browsers automatically follow this. - 302 Found: Temporary redirect.

4xx Client Error: - 400 Bad Request: The server could not understand the request. Usually malformed syntax or missing fields. - 401 Unauthorized: Authentication is required. The client needs to log in. - 403 Forbidden: The client is authenticated but does not have permission. - 404 Not Found: The resource does not exist. - 409 Conflict: The request conflicts with the current state. Like creating a duplicate. - 422 Unprocessable Entity: Validation failed. The request is syntactically correct but semantically wrong. - 429 Too Many Requests: Rate limit exceeded.

5xx Server Error: - 500 Internal Server Error: Something went wrong on the server. Generic catch all. - 502 Bad Gateway: The server got an invalid response from an upstream server. - 503 Service Unavailable: The server is temporarily overloaded or down for maintenance. - 504 Gateway Timeout: The upstream server did not respond in time.

Headers you should know: - Content-Type: Describes the format of the body. application/json, text/html, multipart/form-data. - Authorization: Credentials for authenticating the client. - Accept: What formats the client can accept. - Cache-Control: How responses should be cached. - Set-Cookie: Set a cookie in the browser. - CORS headers: Control which domains can access the API from a browser.

REST API Design Principles

REST is not a standard. It is a set of guidelines for building APIs. Here are the key principles.

Resources are nouns, not verbs. Your URLs should represent resources, not actions. - Good: GET /users, POST /users, GET /users/123 - Bad: GET /getUser, POST /createUser, GET /deleteUser?id=123

Use HTTP methods correctly: - GET for reading data. Never modify data with GET. - POST for creating new resources. - PUT for replacing an entire resource. - PATCH for partial updates. - DELETE for removing resources.

Use plural nouns for collections: - /users (not /user) - /posts (not /post)

Use subresources for related data: - GET /users/123/posts (get all posts by user 123) - GET /posts/456/comments (get all comments on post 456)

Use query parameters for filtering, sorting, and pagination: - GET /posts?status=published&sort=created_at&page=2&limit=20

Return appropriate status codes: - 200 for successful GET and PUT - 201 for successful POST - 204 for successful DELETE - 400 for bad requests - 404 for not found

Version your API: - /api/v1/users - /api/v2/users

JSON

JSON is the standard format for data exchange on the web. It stands for JavaScript Object Notation but it is language independent.

{
  "id": 1,
  "name": "John Doe",
  "email": "john@example.com",
  "age": 30,
  "isActive": true,
  "hobbies": ["reading", "coding", "gaming"],
  "address": {
    "street": "123 Main St",
    "city": "New York",
    "zip": "10001"
  }
}

JSON rules: - Keys must be strings in double quotes. - Strings are in double quotes (not single quotes, not backticks). - Numbers are bare (no quotes). - Booleans are true/false (lowercase). - Null is null (lowercase). - Arrays use square brackets. - Objects use curly braces. - No trailing commas.

Every backend developer needs to be comfortable with JSON. You will spend your entire career sending and receiving JSON.

Phase 5: Databases

Your backend needs to store data somewhere. User accounts, posts, comments, orders, everything needs to persist. That is what databases are for.

SQL vs NoSQL

There are two main types of databases.

SQL databases store data in tables with rows and columns. Think of them like Excel spreadsheets with strict rules. Each table has a fixed schema. Every row in a table has the same columns. You use SQL (Structured Query Language) to query them.

Examples: PostgreSQL, MySQL, SQLite, SQL Server.

NoSQL databases store data in other formats. There are several types: - Document databases like MongoDB store JSON like documents. Each document can have different fields. - Key value stores like Redis store simple key value pairs. - Wide column stores like Cassandra store data in columns rather than rows. - Graph databases like Neo4j store relationships between data points.

For most applications, you want a SQL database. PostgreSQL is the gold standard. It is powerful, reliable, open source, and has been battle tested for decades. It supports advanced features like JSON columns, full text search, and geographic queries.

NoSQL databases are useful in specific scenarios. MongoDB is good when your data does not have a fixed structure and you need to iterate quickly. Redis is used for caching and real time data. Cassandra is for massive scale where you need high write throughput.

PostgreSQL

I recommend PostgreSQL for almost everything. Here is why.

It is open source and free. No licensing fees, no vendor lock in.

It is reliable. It has been around since 1996 and is used by companies like Apple, Instagram, and Red Hat. It handles ACID transactions properly. Your data is safe.

It is powerful. PostgreSQL supports advanced features that other databases do not: - JSON and JSONB columns for semi structured data - Full text search with ranking - Geographic queries with PostGIS - Custom data types - Window functions - Common Table Expressions (CTEs) - Partial indexes - Concurrent index creation

It is extensible. You can add extensions like PostGIS for geospatial data, pgvector for vector similarity search (used in AI applications), and timescaledb for time series data.

SQL Basics

You need to learn SQL. Even if you use an ORM, you need to understand SQL. ORMs generate SQL for you. When the generated SQL is slow, you need to be able to read it and optimize it.

Creating tables:

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    age INTEGER CHECK (age > 0),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE posts (
    id SERIAL PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    content TEXT,
    user_id INTEGER REFERENCES users(id),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Inserting data:

INSERT INTO users (name, email, age)
VALUES ('John Doe', 'john@example.com', 30);

INSERT INTO posts (title, content, user_id)
VALUES ('My First Post', 'Hello everyone!', 1);

Querying data:

-- Basic select
SELECT * FROM users;
SELECT name, email FROM users;

-- Filtering
SELECT * FROM users WHERE age > 18;
SELECT * FROM users WHERE name LIKE 'J%';

-- Sorting
SELECT * FROM users ORDER BY created_at DESC;

-- Limiting
SELECT * FROM users LIMIT 10 OFFSET 20;

-- Joins
SELECT posts.title, users.name
FROM posts
JOIN users ON posts.user_id = users.id;

-- Aggregation
SELECT user_id, COUNT(*) as post_count
FROM posts
GROUP BY user_id
HAVING COUNT(*) > 5
ORDER BY post_count DESC;

-- Subqueries
SELECT name FROM users
WHERE id IN (
    SELECT user_id FROM posts GROUP BY user_id HAVING COUNT(*) > 5
);

Updating data:

UPDATE users SET email = 'newemail@example.com' WHERE id = 1;

Deleting data:

DELETE FROM posts WHERE id = 1;

Indexes

Indexes are the most important performance feature of databases. An index is like the index at the back of a textbook. Instead of reading every page to find what you need, you look at the index and go directly to the right page.

Without an index, a query like WHERE email = 'john@example.com' has to scan every row in the table. With millions of rows, this is slow. With an index on the email column, the database can find the row instantly.

CREATE INDEX idx_users_email ON users(email);

-- For queries that filter by user_id on the posts table
CREATE INDEX idx_posts_user_id ON posts(user_id);

-- For queries that sort by created_at
CREATE INDEX idx_posts_created_at ON posts(created_at);

-- Composite index for queries that filter by both columns
CREATE INDEX idx_posts_user_created ON posts(user_id, created_at);

Do not index everything. Indexes speed up reads but slow down writes. Every time you insert, update, or delete a row, all indexes on that table must be updated too. Add indexes only for queries that your application actually runs.

ORMs

ORMs let you interact with your database using your programming language instead of writing raw SQL. They are convenient but you should understand SQL first before using them.

In Python, SQLAlchemy is the most popular ORM. Here is how it looks:

from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

Base = declarative_base()

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(String)
    email = Column(String)

engine = create_engine('postgresql://localhost/mydb')
Session = sessionmaker(bind=engine)
session = Session()

# Query
users = session.query(User).filter(User.age > 18).all()

In Go, using GORM:

type User struct {
    ID    uint   `gorm:"primaryKey"`
    Name  string
    Email string `gorm:"uniqueIndex"`
}

var user User
db.First(&user, 1)                 // Find by ID
db.Where("name = ?", "John").Find(&users)  // Find with condition

In TypeScript, using Prisma:

const user = await prisma.user.findMany({
    where: { age: { gt: 18 } },
    include: { posts: true }
});

ORMs make common operations easy. But when you need complex queries, you still need to know SQL. ORMs can generate terrible queries if you are not careful. Always check the SQL an ORM generates for complex queries.

Phase 6: Building APIs with Each Language

Now let us build actual APIs with each language in our stack. I will show you the same API built in Go, Python with FastAPI, TypeScript with Express, and Java with Spring Boot.

The API Specification

We are building a simple task management API with these endpoints:

GET    /tasks        List all tasks
POST   /tasks        Create a new task
GET    /tasks/:id    Get a single task
PUT    /tasks/:id    Update a task
DELETE /tasks/:id    Delete a task

Each task has: - id: integer - title: string - completed: boolean - created_at: timestamp

Go with Standard Library

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "strconv"
    "strings"
    "sync"
    "time"
)

type Task struct {
    ID        int       `json:"id"`
    Title     string    `json:"title"`
    Completed bool      `json:"completed"`
    CreatedAt time.Time `json:"created_at"`
}

var (
    tasks  []Task
    nextID int
    mu     sync.Mutex
)

func main() {
    http.HandleFunc("/tasks", handleTasks)
    http.HandleFunc("/tasks/", handleTaskByID)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

func handleTasks(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")

    if r.Method == "GET" {
        mu.Lock()
        resp := tasks
        mu.Unlock()
        json.NewEncoder(w).Encode(resp)
        return
    }

    if r.Method == "POST" {
        var task Task
        if err := json.NewDecoder(r.Body).Decode(&task); err != nil {
            http.Error(w, `{"error":"invalid JSON"}`, 400)
            return
        }
        mu.Lock()
        nextID++
        task.ID = nextID
        task.CreatedAt = time.Now()
        tasks = append(tasks, task)
        mu.Unlock()
        w.WriteHeader(201)
        json.NewEncoder(w).Encode(task)
        return
    }

    http.Error(w, `{"error":"method not allowed"}`, 405)
}

func handleTaskByID(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    idStr := strings.TrimPrefix(r.URL.Path, "/tasks/")
    id, err := strconv.Atoi(idStr)
    if err != nil {
        http.Error(w, `{"error":"invalid id"}`, 400)
        return
    }

    mu.Lock()
    defer mu.Unlock()

    for i, task := range tasks {
        if task.ID == id {
            switch r.Method {
            case "GET":
                json.NewEncoder(w).Encode(task)
                return
            case "PUT":
                var updated Task
                if err := json.NewDecoder(r.Body).Decode(&updated); err != nil {
                    http.Error(w, `{"error":"invalid JSON"}`, 400)
                    return
                }
                updated.ID = id
                updated.CreatedAt = task.CreatedAt
                tasks[i] = updated
                json.NewEncoder(w).Encode(updated)
                return
            case "DELETE":
                tasks = append(tasks[:i], tasks[i+1:]...)
                w.WriteHeader(204)
                return
            }
        }
    }
    http.Error(w, `{"error":"not found"}`, 404)
}

This is a complete working API in Go with no external dependencies. The sync.Mutex handles concurrent access safely.

Python with FastAPI

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List
from datetime import datetime
import uvicorn

app = FastAPI()

class TaskCreate(BaseModel):
    title: str
    completed: bool = False

class Task(TaskCreate):
    id: int
    created_at: datetime

tasks: List[Task] = []
next_id = 1

@app.get("/tasks", response_model=List[Task])
def list_tasks():
    return tasks

@app.post("/tasks", response_model=Task, status_code=201)
def create_task(task_data: TaskCreate):
    global next_id
    task = Task(
        id=next_id,
        title=task_data.title,
        completed=task_data.completed,
        created_at=datetime.now()
    )
    next_id += 1
    tasks.append(task)
    return task

@app.get("/tasks/{task_id}", response_model=Task)
def get_task(task_id: int):
    for task in tasks:
        if task.id == task_id:
            return task
    raise HTTPException(status_code=404, detail="Task not found")

@app.put("/tasks/{task_id}", response_model=Task)
def update_task(task_id: int, task_data: TaskCreate):
    for i, task in enumerate(tasks):
        if task.id == task_id:
            updated = Task(
                id=task_id,
                title=task_data.title,
                completed=task_data.completed,
                created_at=task.created_at
            )
            tasks[i] = updated
            return updated
    raise HTTPException(status_code=404, detail="Task not found")

@app.delete("/tasks/{task_id}", status_code=204)
def delete_task(task_id: int):
    for i, task in enumerate(tasks):
        if task.id == task_id:
            tasks.pop(i)
            return
    raise HTTPException(status_code=404, detail="Task not found")

FastAPI is beautiful. It automatically generates OpenAPI documentation at /docs. It validates request data using Pydantic models. It is fast and intuitive.

TypeScript with Express

import express, { Request, Response } from 'express';

interface Task {
    id: number;
    title: string;
    completed: boolean;
    created_at: Date;
}

interface TaskCreate {
    title: string;
    completed?: boolean;
}

const app = express();
app.use(express.json());

let tasks: Task[] = [];
let nextId = 1;

app.get('/tasks', (_req: Request, res: Response) => {
    res.json(tasks);
});

app.post('/tasks', (req: Request, res: Response) => {
    const data: TaskCreate = req.body;
    const task: Task = {
        id: nextId++,
        title: data.title,
        completed: data.completed || false,
        created_at: new Date()
    };
    tasks.push(task);
    res.status(201).json(task);
});

app.get('/tasks/:id', (req: Request, res: Response) => {
    const id = parseInt(req.params.id);
    const task = tasks.find(t => t.id === id);
    if (!task) {
        res.status(404).json({ error: 'Task not found' });
        return;
    }
    res.json(task);
});

app.put('/tasks/:id', (req: Request, res: Response) => {
    const id = parseInt(req.params.id);
    const index = tasks.findIndex(t => t.id === id);
    if (index === -1) {
        res.status(404).json({ error: 'Task not found' });
        return;
    }
    const data: TaskCreate = req.body;
    tasks[index] = {
        ...tasks[index],
        title: data.title,
        completed: data.completed ?? tasks[index].completed
    };
    res.json(tasks[index]);
});

app.delete('/tasks/:id', (req: Request, res: Response) => {
    const id = parseInt(req.params.id);
    const index = tasks.findIndex(t => t.id === id);
    if (index === -1) {
        res.status(404).json({ error: 'Task not found' });
        return;
    }
    tasks.splice(index, 1);
    res.status(204).send();
});

app.listen(3000, () => {
    console.log('Server running on port 3000');
});

Java with Spring Boot

// Task.java
public class Task {
    private int id;
    private String title;
    private boolean completed;
    private LocalDateTime createdAt;

    // Constructors, getters, setters
    public Task() {}

    public Task(int id, String title, boolean completed, LocalDateTime createdAt) {
        this.id = id;
        this.title = title;
        this.completed = completed;
        this.createdAt = createdAt;
    }

    public int getId() { return id; }
    public void setId(int id) { this.id = id; }
    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }
    public boolean isCompleted() { return completed; }
    public void setCompleted(boolean completed) { this.completed = completed; }
    public LocalDateTime getCreatedAt() { return createdAt; }
    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
}

// TaskController.java
@RestController
@RequestMapping("/tasks")
public class TaskController {

    private List<Task> tasks = new ArrayList<>();
    private int nextId = 1;

    @GetMapping
    public List<Task> getAllTasks() {
        return tasks;
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Task createTask(@RequestBody Task task) {
        task.setId(nextId++);
        task.setCreatedAt(LocalDateTime.now());
        tasks.add(task);
        return task;
    }

    @GetMapping("/{id}")
    public Task getTask(@PathVariable int id) {
        return tasks.stream()
            .filter(t -> t.getId() == id)
            .findFirst()
            .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Task not found"));
    }

    @PutMapping("/{id}")
    public Task updateTask(@PathVariable int id, @RequestBody Task updated) {
        Task task = getTask(id);
        task.setTitle(updated.getTitle());
        task.setCompleted(updated.isCompleted());
        return task;
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void deleteTask(@PathVariable int id) {
        Task task = getTask(id);
        tasks.remove(task);
    }
}

Spring Boot is more verbose but it scales to enterprise applications. It has built in dependency injection, security, transaction management, and much more.

Phase 7: Connecting to a Database

Now let us connect our APIs to a real database. We will use PostgreSQL for all examples.

Setting Up PostgreSQL

Install PostgreSQL on your machine. On macOS, use Homebrew:

brew install postgresql
brew services start postgresql

Create a database:

createdb taskmanager

Connect to it:

psql taskmanager

Create the tasks table:

CREATE TABLE tasks (
    id SERIAL PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    completed BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Python with psycopg2

import psycopg2
import psycopg2.extras
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from datetime import datetime

app = FastAPI()

conn = psycopg2.connect(
    host="localhost",
    database="taskmanager",
    user="postgres",
    password="postgres"
)

class TaskCreate(BaseModel):
    title: str
    completed: bool = False

class Task(TaskCreate):
    id: int
    created_at: datetime

@app.get("/tasks")
def list_tasks():
    cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
    cur.execute("SELECT * FROM tasks ORDER BY created_at DESC")
    tasks = cur.fetchall()
    cur.close()
    return tasks

@app.post("/tasks", status_code=201)
def create_task(task: TaskCreate):
    cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
    cur.execute(
        "INSERT INTO tasks (title, completed) VALUES (%s, %s) RETURNING *",
        (task.title, task.completed)
    )
    conn.commit()
    new_task = cur.fetchone()
    cur.close()
    return new_task

@app.get("/tasks/{task_id}")
def get_task(task_id: int):
    cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
    cur.execute("SELECT * FROM tasks WHERE id = %s", (task_id,))
    task = cur.fetchone()
    cur.close()
    if not task:
        raise HTTPException(status_code=404, detail="Task not found")
    return task

@app.put("/tasks/{task_id}")
def update_task(task_id: int, task: TaskCreate):
    cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
    cur.execute(
        "UPDATE tasks SET title = %s, completed = %s WHERE id = %s RETURNING *",
        (task.title, task.completed, task_id)
    )
    conn.commit()
    updated = cur.fetchone()
    cur.close()
    if not updated:
        raise HTTPException(status_code=404, detail="Task not found")
    return updated

@app.delete("/tasks/{task_id}", status_code=204)
def delete_task(task_id: int):
    cur = conn.cursor()
    cur.execute("DELETE FROM tasks WHERE id = %s", (task_id,))
    conn.commit()
    if cur.rowcount == 0:
        cur.close()
        raise HTTPException(status_code=404, detail="Task not found")
    cur.close()

Go with database/sql

package main

import (
    "database/sql"
    "encoding/json"
    "log"
    "net/http"
    "strconv"
    "time"
    _ "github.com/lib/pq"
)

type Task struct {
    ID        int       `json:"id"`
    Title     string    `json:"title"`
    Completed bool      `json:"completed"`
    CreatedAt time.Time `json:"created_at"`
}

var db *sql.DB

func main() {
    var err error
    db, err = sql.Open("postgres",
        "host=localhost dbname=taskmanager user=postgres password=postgres sslmode=disable")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    http.HandleFunc("/tasks", handleTasks)
    http.HandleFunc("/tasks/", handleTaskByID)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

func handleTasks(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")

    if r.Method == "GET" {
        rows, err := db.Query("SELECT id, title, completed, created_at FROM tasks ORDER BY created_at DESC")
        if err != nil {
            http.Error(w, `{"error":"database error"}`, 500)
            return
        }
        defer rows.Close()

        var tasks []Task
        for rows.Next() {
            var t Task
            rows.Scan(&t.ID, &t.Title, &t.Completed, &t.CreatedAt)
            tasks = append(tasks, t)
        }
        json.NewEncoder(w).Encode(tasks)
        return
    }

    if r.Method == "POST" {
        var task Task
        json.NewDecoder(r.Body).Decode(&task)
        err := db.QueryRow(
            "INSERT INTO tasks (title, completed) VALUES ($1, $2) RETURNING id, created_at",
            task.Title, task.Completed,
        ).Scan(&task.ID, &task.CreatedAt)
        if err != nil {
            http.Error(w, `{"error":"database error"}`, 500)
            return
        }
        w.WriteHeader(201)
        json.NewEncoder(w).Encode(task)
        return
    }

    http.Error(w, `{"error":"method not allowed"}`, 405)
}

TypeScript with Prisma

First, set up Prisma:

npm install prisma @prisma/client
npx prisma init

Schema (prisma/schema.prisma):

generator client {
    provider = "prisma-client-js"
}

datasource db {
    provider = "postgresql"
    url      = env("DATABASE_URL")
}

model Task {
    id        Int      @id @default(autoincrement())
    title     String
    completed Boolean  @default(false)
    createdAt DateTime @default(now()) @map("created_at")
    @@map("tasks")
}

API code:

import express, { Request, Response } from 'express';
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();
const app = express();
app.use(express.json());

app.get('/tasks', async (_req: Request, res: Response) => {
    const tasks = await prisma.task.findMany({
        orderBy: { createdAt: 'desc' }
    });
    res.json(tasks);
});

app.post('/tasks', async (req: Request, res: Response) => {
    const task = await prisma.task.create({
        data: {
            title: req.body.title,
            completed: req.body.completed || false
        }
    });
    res.status(201).json(task);
});

app.get('/tasks/:id', async (req: Request, res: Response) => {
    const id = parseInt(req.params.id);
    const task = await prisma.task.findUnique({ where: { id } });
    if (!task) {
        res.status(404).json({ error: 'Not found' });
        return;
    }
    res.json(task);
});

app.put('/tasks/:id', async (req: Request, res: Response) => {
    const id = parseInt(req.params.id);
    const task = await prisma.task.update({
        where: { id },
        data: {
            title: req.body.title,
            completed: req.body.completed
        }
    });
    res.json(task);
});

app.delete('/tasks/:id', async (req: Request, res: Response) => {
    const id = parseInt(req.params.id);
    await prisma.task.delete({ where: { id } });
    res.status(204).send();
});

app.listen(3000, () => console.log('Running on 3000'));

Phase 8: Authentication and Security

Most applications need to know who the user is. Authentication is how you verify identity. Authorization is what you allow them to do.

How Authentication Works

The flow is simple. The user sends their credentials (email and password). The server checks if they are correct. If yes, the server gives the user a token. The user sends this token with every subsequent request. The server verifies the token to know who the user is.

Password Hashing

Never store passwords in plain text. If your database is breached, all user passwords are exposed. Use hashing algorithms like bcrypt.

In Python with bcrypt:

import bcrypt

def hash_password(password: str) -> str:
    salt = bcrypt.gensalt()
    return bcrypt.hashpw(password.encode(), salt).decode()

def verify_password(password: str, hashed: str) -> bool:
    return bcrypt.checkpw(password.encode(), hashed.encode())

In Go with golang.org/x/crypto:

import "golang.org/x/crypto/bcrypt"

func hashPassword(password string) (string, error) {
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
    return string(bytes), err
}

func checkPassword(password, hash string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
    return err == nil
}

In TypeScript with bcryptjs:

import bcrypt from 'bcryptjs';

async function hashPassword(password: string): Promise<string> {
    const salt = await bcrypt.genSalt(10);
    return bcrypt.hash(password, salt);
}

async function verifyPassword(password: string, hash: string): Promise<boolean> {
    return bcrypt.compare(password, hash);
}

JWT Tokens

JWT is the standard way to handle authentication in APIs. When a user logs in, the server creates a signed token. The client stores this token and sends it with every request in the Authorization header.

A JWT has three parts separated by dots: - Header: Contains the type of token and the signing algorithm - Payload: Contains the data (user ID, expiration time, etc.) - Signature: Verifies that the token has not been tampered with

The token looks like this:

eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxfQ.8x8x8x8x8x8x8x8x8x8x8x8x8x8x8x8x8x8x8x8x

Python with PyJWT:

import jwt
from datetime import datetime, timedelta

SECRET_KEY = "your-secret-key-keep-it-safe"

def create_token(user_id: int) -> str:
    payload = {
        "user_id": user_id,
        "exp": datetime.utcnow() + timedelta(days=7),
        "iat": datetime.utcnow()
    }
    return jwt.encode(payload, SECRET_KEY, algorithm="HS256")

def verify_token(token: str):
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
        return payload["user_id"]
    except jwt.ExpiredSignatureError:
        return None
    except jwt.InvalidTokenError:
        return None

Go with golang-jwt:

import (
    "time"
    "github.com/golang-jwt/jwt/v5"
)

var secretKey = []byte("your-secret-key-keep-it-safe")

type Claims struct {
    UserID int `json:"user_id"`
    jwt.RegisteredClaims
}

func createToken(userID int) (string, error) {
    claims := Claims{
        UserID: userID,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(7 * 24 * time.Hour)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
        },
    }
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString(secretKey)
}

func verifyToken(tokenStr string) (int, error) {
    token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (interface{}, error) {
        return secretKey, nil
    })
    if err != nil {
        return 0, err
    }
    claims := token.Claims.(*Claims)
    return claims.UserID, nil
}

Protecting Routes

In FastAPI, you create a dependency that extracts and verifies the token:

from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials

security = HTTPBearer()

def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
    user_id = verify_token(credentials.credentials)
    if user_id is None:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid or expired token"
        )
    return user_id

@app.get("/protected")
def protected_route(user_id: int = Depends(get_current_user)):
    return {"message": f"Hello user {user_id}"}

In Go, you create middleware:

func authMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        tokenStr := r.Header.Get("Authorization")
        if len(tokenStr) < 7 || tokenStr[:7] != "Bearer " {
            http.Error(w, `{"error":"unauthorized"}`, 401)
            return
        }
        userID, err := verifyToken(tokenStr[7:])
        if err != nil {
            http.Error(w, `{"error":"unauthorized"}`, 401)
            return
        }
        r.Header.Set("X-User-ID", strconv.Itoa(userID))
        next(w, r)
    }
}

Basic Security Checklist

Phase 9: Testing

If you do not test your code, you will break things. It is inevitable. Testing gives you confidence that your code works correctly.

Types of Tests

Unit tests test individual functions or methods in isolation. They are fast and focused. They do not touch the database or the network.

Integration tests test how different parts of your system work together. For example, testing that your API correctly reads and writes to the database.

End to end tests test the entire system from the user perspective. They make real HTTP requests and verify the full response.

For backend development, focus on unit tests and integration tests. E2E tests are slow and brittle.

Unit Tests

Python with pytest:

# app.py
def calculate_discount(price: float, discount_percent: float) -> float:
    if discount_percent < 0 or discount_percent > 100:
        raise ValueError("Discount must be between 0 and 100")
    return price * (1 - discount_percent / 100)

# test_app.py
import pytest
from app import calculate_discount

def test_calculate_discount():
    assert calculate_discount(100, 10) == 90
    assert calculate_discount(100, 0) == 100
    assert calculate_discount(100, 100) == 0

def test_invalid_discount():
    with pytest.raises(ValueError):
        calculate_discount(100, -1)
    with pytest.raises(ValueError):
        calculate_discount(100, 101)

Go with testing package:

// math.go
func CalculateDiscount(price, discountPercent float64) (float64, error) {
    if discountPercent < 0 || discountPercent > 100 {
        return 0, fmt.Errorf("discount must be between 0 and 100")
    }
    return price * (1 - discountPercent/100), nil
}

// math_test.go
func TestCalculateDiscount(t *testing.T) {
    result, err := CalculateDiscount(100, 10)
    if err != nil || result != 90 {
        t.Errorf("Expected 90, got %f", result)
    }
}

TypeScript with Jest:

// math.ts
export function calculateDiscount(price: number, discountPercent: number): number {
    if (discountPercent < 0 || discountPercent > 100) {
        throw new Error("Discount must be between 0 and 100");
    }
    return price * (1 - discountPercent / 100);
}

// math.test.ts
import { calculateDiscount } from './math';

test('calculates discount correctly', () => {
    expect(calculateDiscount(100, 10)).toBe(90);
    expect(calculateDiscount(100, 0)).toBe(100);
    expect(calculateDiscount(100, 100)).toBe(0);
});

Integration Tests

Integration tests test your API endpoints with a real database.

Python with FastAPI TestClient:

from fastapi.testclient import TestClient
from main import app

client = TestClient(app)

def test_create_task():
    response = client.post("/tasks", json={
        "title": "Test task",
        "completed": False
    })
    assert response.status_code == 201
    data = response.json()
    assert data["title"] == "Test task"
    assert "id" in data

def test_list_tasks():
    response = client.get("/tasks")
    assert response.status_code == 200
    assert isinstance(response.json(), list)

Go with httptest:

func TestCreateTask(t *testing.T) {
    body := `{"title":"Test task","completed":false}`
    req := httptest.NewRequest("POST", "/tasks", strings.NewReader(body))
    req.Header.Set("Content-Type", "application/json")
    w := httptest.NewRecorder()
    handleTasks(w, req)
    resp := w.Result()
    if resp.StatusCode != 201 {
        t.Errorf("Expected 201, got %d", resp.StatusCode)
    }
}

Test Fixtures and Factories

In real applications, you need to set up test data before each test and clean up after.

# conftest.py
import pytest
from main import app
from database import get_db, init_db, clear_db

@pytest.fixture
def client():
    init_db()
    app.dependency_overrides[get_db] = lambda: test_db
    with TestClient(app) as c:
        yield c
    clear_db()

def test_get_tasks(client):
    # Create a task first
    client.post("/tasks", json={"title": "Test"})
    # Now list tasks
    response = client.get("/tasks")
    assert len(response.json()) == 1

What to Test

Test the happy path. Does it work when everything is correct? Test error cases. What happens when data is invalid? What happens when a resource is not found? Test edge cases. Empty lists. Very long strings. Maximum values.

Do not test the framework. Do not test things that are already tested by the library authors. Focus on testing your logic.

Phase 10: Deployment

Building an API on your laptop is fun. But the real value comes when other people can use it. That means deploying it to a server.

Docker

Docker packages your application and all its dependencies into a container. This solves the “it works on my machine” problem.

A Dockerfile for Python:

FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

A Dockerfile for Go:

FROM golang:1.22 AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o server .

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/server .
EXPOSE 8080
CMD ["./server"]

Note the multi stage build. The first stage compiles the Go binary. The second stage is a tiny Alpine image with just the binary. The resulting image is about 15 MB instead of 1 GB.

A Dockerfile for TypeScript:

FROM node:20-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package*.json ./
EXPOSE 3000
CMD ["node", "dist/index.js"]

Docker Compose

For applications that need a database, use Docker Compose to run multiple containers.

version: '3.8'

services:
  app:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://postgres:postgres@db:5432/taskmanager
    depends_on:
      - db

  db:
    image: postgres:16
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=taskmanager
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

volumes:
  postgres_data:

Deploying to a Server

The simplest approach: rent a VPS from Digital Ocean, Linode, or Hetzner. Install Docker on it. Copy your code. Run it.

# SSH into your server
ssh root@your-server-ip

# Install Docker
curl -fsSL https://get.docker.com | sh

# Copy your project
# From your local machine:
scp -r your-project root@your-server-ip:~/app

# On the server, run it
cd ~/app
docker-compose up -d

CI/CD with GitHub Actions

Set up automatic deployment when you push to GitHub.

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run tests
        run: |
          docker-compose -f docker-compose.test.yml up --abort-on-container-exit

  deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to server
        uses: appleboy/ssh-action@v1.0.0
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SERVER_SSH_KEY }}
          script: |
            cd ~/app
            git pull
            docker-compose up -d --build

Cloud Platforms

For simpler deployment, use platforms that handle infrastructure for you.

Railway: Connect your GitHub repository and it automatically deploys. It handles domains, SSL, and scaling. Great for beginners.

Render: Similar to Railway. Free tier available. Supports Docker, static sites, and background workers.

Fly.io: Runs your Docker containers on servers close to your users. Good for global applications.

Digital Ocean App Platform: Deploy directly from GitHub. Handles scaling and SSL.

Phase 11: Caching with Redis

As your application grows, you need to make it faster. Caching is the most effective way to improve performance.

Why Caching

Reading from a database is slow compared to reading from memory. If the same data is requested many times, you can store it in a cache after the first request and serve it from memory for subsequent requests.

A typical database query takes 10-50 milliseconds. A Redis lookup takes less than 1 millisecond. If you have a thousand requests per second for the same data, caching saves you 10-50 seconds of database time per second.

Redis Basics

Redis is an in memory data store. It stores key value pairs. The keys are strings. The values can be strings, lists, sets, hashes, or more complex data structures.

Install Redis:

brew install redis
brew services start redis

Python with redis-py:

import redis
import json

cache = redis.Redis(host='localhost', port=6379, decode_responses=True)

# Set a value with 5 minute expiration
cache.setex("user:123", 300, json.dumps({"name": "John"}))

# Get a value
data = cache.get("user:123")
if data:
    user = json.loads(data)
else:
    user = query_database(123)
    cache.setex("user:123", 300, json.dumps(user))

Go with go-redis:

import "github.com/redis/go-redis/v9"

var rdb = redis.NewClient(&redis.Options{
    Addr: "localhost:6379",
})

func getUser(id int) (*User, error) {
    ctx := context.Background()

    // Try cache first
    data, err := rdb.Get(ctx, fmt.Sprintf("user:%d", id)).Result()
    if err == nil {
        var user User
        json.Unmarshal([]byte(data), &user)
        return &user, nil
    }

    // Cache miss, query database
    user, err := queryDB(id)
    if err != nil {
        return nil, err
    }

    // Store in cache
    jsonData, _ := json.Marshal(user)
    rdb.Set(ctx, fmt.Sprintf("user:%d", id), jsonData, 5*time.Minute)

    return user, nil
}

Caching Strategies

Cache aside (lazy loading): 1. Check the cache. 2. If found (cache hit), return the data. 3. If not found (cache miss), query the database. 4. Store the result in the cache with an expiration time. 5. Return the data.

This is the most common pattern. It works well for read heavy workloads.

Write through: 1. When data is updated, write to both the database and the cache. 2. The cache always has fresh data. 3. No cache misses for updated data.

But this means every write goes to both the database and cache, which adds latency to writes.

Cache invalidation: The hardest part of caching is knowing when to invalidate (delete) cached data. If data changes, the cached version is stale.

Strategies for invalidation: - Set a short TTL (time to live). The cache expires automatically. Simple but data might be stale for up to the TTL duration. - Invalidate on write. When data is updated, delete the cache key. The next read will fetch fresh data. - Use a version number. Increment a version number when data changes and include it in the cache key.

What to Cache

Cache database query results that are queried frequently but change infrequently. User profiles, product listings, configuration settings.

Cache computed results. If you have an expensive calculation, cache the result.

Do not cache data that changes constantly. Real time stock prices, live scores, chat messages.

Do not cache sensitive data unless you are careful about access control.

Phase 12: Message Queues and Async Processing

Some operations should not happen immediately during a request. Sending an email, processing an image, generating a PDF. These take time and the user does not need to wait for them.

Message queues solve this. You put a task in a queue. A background worker picks it up and processes it.

The Pattern

The API receives a request. It creates a task message and puts it in a queue. It immediately responds to the user saying “request accepted.” A background worker picks up the task, processes it, and maybe stores the result.

This pattern is called asynchronous processing. The user gets a fast response. The slow work happens in the background.

RabbitMQ

RabbitMQ is a popular message broker. It accepts messages from producers, stores them in queues, and delivers them to consumers.

Python with Celery and RabbitMQ:

from celery import Celery

app = Celery('tasks', broker='amqp://guest@localhost:5672//')

@app.task
def send_welcome_email(user_id):
    user = get_user(user_id)
    # Send email (slow operation)
    send_email(user.email, "Welcome to our platform!")
    return f"Email sent to {user.email}"

@app.task
def generate_thumbnail(image_path):
    # Process image (slow operation)
    thumbnail = resize_image(image_path, width=200)
    save_image(thumbnail, image_path + "_thumb.jpg")
    return "Thumbnail generated"

In your API:

@app.post("/signup")
def signup(user_data):
    user = create_user(user_data)
    send_welcome_email.delay(user.id)
    return {"message": "User created", "id": user.id}

The .delay() method puts the task in the queue. The response is sent immediately. Celery workers running in separate processes pick up and execute the tasks.

Use Cases for Async Processing

Sending emails. Whether it is welcome emails, password resets, or notifications, emails are slow and should be async.

Processing uploads. Images, videos, PDFs. Generate thumbnails, compress videos, extract text. Do this in the background.

Generating reports. PDF reports, Excel exports, data analysis. These can take minutes. Run them in the background.

Synchronizing with external services. Webhooks, API calls to third parties. Do not make your users wait for external services.

Scheduled tasks. Daily emails, cleanup jobs, data aggregation. These are not triggered by user actions but run on a schedule. Celery Beat handles scheduled tasks.

Phase 13: Monitoring and Observability

When your application is deployed and real users are using it, you need to know what is happening. Is it running? Is it slow? Are there errors?

Logging

Every request should be logged. When something goes wrong, logs are the first place you look.

Python logging:

import logging

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

@app.get("/tasks")
def list_tasks():
    logger.info("Listing all tasks")
    # ...
    logger.debug(f"Found {len(tasks)} tasks")
    return tasks

Go logging:

import "log"

func handleTasks(w http.ResponseWriter, r *http.Request) {
    log.Printf("Method=%s Path=%s RemoteAddr=%s", r.Method, r.URL.Path, r.RemoteAddr)
    // ...
}

Structured logging is better than plain text. It outputs JSON that can be parsed by log aggregation tools.

import structlog

logger = structlog.get_logger()
logger.info("user.login", user_id=123, ip_address="192.168.1.1")

Health Checks

Add a health check endpoint that monitoring tools can ping.

@app.get("/health")
def health():
    return {
        "status": "healthy",
        "database": check_database(),
        "redis": check_redis(),
        "uptime": get_uptime()
    }

Error Tracking with Sentry

Sentry captures and aggregates errors from your application. It tells you what broke, how often, and on which line of code.

import sentry_sdk

sentry_sdk.init(dsn="your-sentry-dsn")

# Sentry automatically captures unhandled exceptions
# You can also manually capture
try:
    risky_operation()
except Exception as e:
    sentry_sdk.capture_exception(e)

Metrics

Prometheus collects metrics from your application. Grafana visualizes them.

Add a metrics endpoint that Prometheus scrapes:

from prometheus_client import Counter, Histogram, generate_latest

REQUEST_COUNT = Counter('http_requests_total', 'Total HTTP requests', ['method', 'endpoint'])
REQUEST_DURATION = Histogram('http_request_duration_seconds', 'HTTP request duration', ['method', 'endpoint'])

@app.middleware("http")
async def metrics_middleware(request, call_next):
    REQUEST_COUNT.labels(method=request.method, endpoint=request.url.path).inc()
    start = time.time()
    response = await call_next(request)
    duration = time.time() - start
    REQUEST_DURATION.labels(method=request.method, endpoint=request.url.path).observe(duration)
    return response

@app.get("/metrics")
def metrics():
    return Response(content=generate_latest(), media_type="text/plain")

Phase 14: Advanced Topics

Once you have mastered the basics, there is always more to learn. Here are some advanced topics to explore.

Scaling

Horizontal scaling means adding more servers. Vertical scaling means making your server more powerful.

For most applications, horizontal scaling is better. You can handle more traffic by adding more instances of your application behind a load balancer. If one server fails, the others keep running.

But horizontal scaling introduces complexity. You need a load balancer to distribute traffic. You need to handle sessions and state across multiple servers. You need to coordinate database connections.

Load Balancing

A load balancer distributes incoming traffic across multiple servers. Nginx is commonly used as a load balancer and reverse proxy.

upstream backend {
    server app1:8000;
    server app2:8000;
    server app3:8000;
}

server {
    listen 80;
    location / {
        proxy_pass http://backend;
    }
}

Rate Limiting

Rate limiting prevents abuse by limiting how many requests a client can make in a given time period.

import time
from collections import defaultdict

class RateLimiter:
    def __init__(self, max_requests: int, window: int):
        self.max_requests = max_requests
        self.window = window
        self.requests = defaultdict(list)

    def is_allowed(self, client_id: str) -> bool:
        now = time.time()
        self.requests[client_id] = [
            t for t in self.requests[client_id]
            if now - t < self.window
        ]
        if len(self.requests[client_id]) >= self.max_requests:
            return False
        self.requests[client_id].append(now)
        return True

Database Scaling

As your data grows, a single database might not be enough.

Read replicas. You have one primary database that handles writes and multiple replicas that handle reads. All writes go to the primary. The primary replicates data to the replicas. Reads can go to any replica.

Read replicas are relatively simple to set up. They help when your application is read heavy (most applications are).

Sharding. You split your data across multiple databases. Each database holds a subset of the data. User IDs 1-10000 go to database 1. User IDs 10001-20000 go to database 2.

Sharding is complex. You need a strategy for distributing data and routing queries. It should be your last resort after trying everything else.

CQRS

Command Query Responsibility Segregation. You separate commands (writes) and queries (reads) into different models.

The write model uses a normalized database optimized for data integrity. The read model uses a denormalized database optimized for fast reads.

CQRS adds complexity but gives you flexibility. You can scale reads and writes independently. You can use different database technologies for each.

Event Sourcing

Instead of storing the current state of your data, you store every change as an event. The current state is derived by replaying all events.

Event sourcing gives you a complete audit trail. You can go back to any point in time and see the state. You can debug issues by replaying events.

But it is complex and unfamiliar to most developers. Use it only when you need the audit trail.

Projects Portfolio

This is the most important section. Reading will not make you a backend developer. Building will. Here are projects organized by skill level.

Beginner Projects

Project 1: Personal Blog API Build an API for a personal blog. It should have endpoints for creating, reading, updating, and deleting posts. Store data in PostgreSQL. Add user authentication so only you can create and edit posts. Add pagination for listing posts. Tech: Python + FastAPI + PostgreSQL Stretch goals: Add comments. Add categories and tags. Add a search endpoint.

Project 2: URL Shortener Build a service like Bitly. Users submit a long URL and get back a short URL. When someone visits the short URL via a browser, they get redirected to the original URL. Track how many times each short URL is visited. Tech: Go + Redis + PostgreSQL Stretch goals: Add expiration times for URLs. Add custom short codes. Add analytics dashboard.

Project 3: Library Management System Build an API for managing a library. Track books, members, and borrowing. A member can borrow a book if it is available. They return it after some days. Track due dates and calculate fines. Send reminders for overdue books. Tech: TypeScript + Express + PostgreSQL + Prisma Stretch goals: Send email notifications. Add a search interface. Add a reservation system.

Intermediate Projects

Project 4: Real Time Chat Application Build a chat application where users can create rooms and send messages in real time. Messages should be stored in a database. Users should be able to see message history when they join a room. Support multiple users in the same room. Tech: Go or Python + WebSockets + PostgreSQL + Redis Stretch goals: Add file sharing. Add typing indicators. Add message search.

Project 5: E Commerce API Build the backend for an e commerce application. Users can browse products, add them to a cart, place orders, and view order history. Handle inventory management. Add payment integration with Stripe. Send order confirmation emails. Tech: Python + FastAPI + PostgreSQL + Celery + Stripe API Stretch goals: Add product reviews. Add discount codes. Add admin dashboard.

Project 6: Task Management System Build a project management tool like Trello. Users can create boards, lists, and cards. Cards can be moved between lists with drag and drop (WebSocket updates). Add comments and due dates. Support multiple users on the same board. Tech: TypeScript + Express + PostgreSQL + WebSockets + Redis Stretch goals: Add file attachments. Add notifications. Add board templates.

Java Projects (For Enterprise Employability)

Project 7: Employee Management System Build a REST API for managing employees. CRUD operations for employees, departments, and roles. Add search and filtering. Add reporting endpoints for department statistics. Use Spring Boot with Spring Data JPA and PostgreSQL. Tech: Java + Spring Boot + Spring Data JPA + PostgreSQL Stretch goals: Add authentication with Spring Security. Add Excel export. Add audit logging.

Project 8: Banking Application Build a banking API. Users can create accounts, deposit money, withdraw money, and transfer money between accounts. Handle concurrent transactions safely. Maintain transaction history. Add interest calculation for savings accounts. Tech: Java + Spring Boot + Spring Data JPA + PostgreSQL + RabbitMQ Stretch goals: Add scheduled interest payments. Add currency conversion. Add fraud detection.

Advanced Projects

Project 9: URL Shortener at Scale Take your URL shortener and scale it. Design it to handle millions of URLs and billions of redirects. Use Redis for caching popular URLs. Use a CDN for redirects. Implement rate limiting. Deploy with Docker and Kubernetes. Tech: Go + Redis + PostgreSQL + Kubernetes + CDN Stretch goals: Add analytics pipeline with Kafka. Add A/B testing for redirects.

Project 10: Video Processing Pipeline Build a system where users upload videos and the system processes them in the background. Generate thumbnails, transcode to different formats (360p, 720p, 1080p), and make them available for streaming. Use a message queue for the processing pipeline. Store videos in S3 compatible storage. Tech: Python + Celery + RabbitMQ + FFmpeg + MinIO + PostgreSQL Stretch goals: Add subtitles. Add video search. Add content moderation.

Project 11: Distributed Task Scheduler Build a distributed cron system. Users can schedule tasks to run at specific times or on intervals. The system should handle millions of scheduled tasks, retry failed tasks, and distribute the workload across multiple workers. Provide a dashboard to view task history. Tech: Go + Redis + PostgreSQL + gRPC Stretch goals: Add task dependencies. Add rate limiting. Add webhook notifications.

Project Guidelines

For every project: 1. Plan the database schema before writing any code. Draw it out. Think about relationships. 2. Design the API endpoints and their request/response formats. Document them. 3. Write the code with proper error handling. Validate all input. 4. Add tests. At least for the critical paths. 5. Write a README explaining how to set up and run the project. 6. Push it to GitHub with clean commit history. 7. Deploy it so it is accessible from the internet. 8. Add a CI/CD pipeline that automatically tests and deploys.

Each project should be on your GitHub. When you apply for jobs, your GitHub is your resume. A well maintained GitHub with several projects is worth more than a degree.

Common Mistakes to Avoid

I have seen many beginners make the same mistakes. Here they are so you can avoid them.

Trying to Learn Everything

You cannot learn everything. There are too many tools, frameworks, and languages. Pick a stack and go deep. You can learn other things later.

Not Building Enough

Reading and watching tutorials feels like progress. It is not. Real progress happens when you write code, hit bugs, and fix them. Build more than you read.

Tutorial Hell

Watching tutorial after tutorial without building anything. You feel like you are learning but you are not. The way out is to close the tutorial and build something. Even if you do not know how. You will figure it out.

Premature Optimization

Do not worry about scaling before you have users. Do not worry about microservices before you have a monolith. Do not worry about performance before you have a working product. Build it. Make it work. Then make it fast.

Not Using Version Control

If you are not using Git, fix that today. It is non negotiable.

Ignoring Testing

Skipping tests feels faster in the short term. It is slower in the long term. Untested code breaks. When it breaks, you spend hours debugging what a test would have caught in seconds.

Overengineering

A simple CRUD API does not need microservices. A blog does not need Kubernetes. Use simple solutions. Add complexity only when you need it.

Comparing Yourself to Others

There will always be someone who knows more than you. That is fine. Compare yourself to who you were yesterday, not to who someone else is today.

Giving Up

This is the most common mistake. Programming is hard. You will get stuck. You will feel stupid. Everyone goes through this. The people who succeed are not the smartest. They are the ones who did not quit.

Conclusion

Becoming a backend developer is a journey. It takes time. It takes patience. It takes a lot of failed attempts and frustrating bugs.

But it is worth it.

There is something incredibly satisfying about building something that other people use. When you deploy your first application and see someone else interact with it, that feeling is unmatched.

The backend is where the real engineering happens. It is where you deal with data, scale, reliability, and performance. It is challenging. It is fun. It is a career that will never be boring.

Start today. Open your terminal. Write some code. Build something. Even if it is small. Even if it is ugly. Even if nobody will ever see it. Just start.

The path we covered in this guide is: 1. Learn the fundamentals. How computers work, networking, Linux. 2. Learn Python. Build CLI projects. 3. Learn Git and GitHub. 4. Learn databases and SQL. 5. Build APIs with FastAPI. 6. Learn Go. Build concurrent systems. 7. Learn TypeScript. Build full stack applications. 8. Learn Java with Spring Boot for enterprise roles. 9. Learn Docker and deployment. 10. Learn caching with Redis. 11. Learn message queues for async processing. 12. Learn monitoring and observability. 13. Build real projects. Deploy them. Put them on GitHub.

And when you get stuck, remember that every expert was once a beginner. The only difference is they did not quit.

Good luck. Now go build something.