A web server is the software responsible for receiving requests from clients, processing those requests, and sending responses back.
When you visit a website, your browser is essentially saying:
“Give me this resource.”
The web server receives that request, figures out what to return, and sends a response.
You can build a simple web server yourself to understand what happens behind frameworks such as Express, Django, Laravel, and Spring Boot.
1. How a Web Server Works
The basic communication looks like this:
Browser
|
| HTTP Request
↓
Web Server
|
| Process request
↓
Application / Files / Database
|
| HTTP Response
↓
Browser
For example, when you request:
GET /index.html HTTP/1.1
Host: example.com
the server may respond with:
HTTP/1.1 200 OK
Content-Type: text/html
<h1>Hello World</h1>
The browser then renders the HTML.
2. What You Need
A basic web server needs a few fundamental components:
- Network socket to communicate over TCP
- Port to listen for connections
- HTTP request parser
- Routing logic
- Response generator
- Static file handling
- Error handling
For a learning project, you don’t need Nginx or Apache. You can build a small HTTP server using a programming language’s built-in networking capabilities.
3. Build One With Node.js
Node.js makes this particularly straightforward because it provides the built-in http module.
Create a file:
server.js
Add:
const http = require("http");
const server = http.createServer((req, res) => {
res.writeHead(200, {
"Content-Type": "text/plain"
});
res.end("Hello from my web server!");
});
server.listen(3000, () => {
console.log("Server running on http://localhost:3000");
});
Start it:
node server.js
Then open:
http://localhost:3000
Your browser sends a request and your server sends back:
Hello from my web server!
That’s a real web server, albeit a very small one.
4. Understanding the Code
This line creates the server:
http.createServer()
The callback receives two important objects:
(req, res)
req
req represents the incoming request.
You can inspect:
req.method
req.url
req.headers
For example:
console.log(req.method);
console.log(req.url);
A request to:
/about
could produce:
GET
/about
res
res represents the response you send back to the client.
For example:
res.writeHead(200);
res.end("Hello!");
The 200 means:
OK
5. Add Routing
A useful server needs to respond differently depending on the URL.
const http = require("http");
const server = http.createServer((req, res) => {
if (req.url === "/" && req.method === "GET") {
res.writeHead(200, {
"Content-Type": "text/plain"
});
return res.end("Home Page");
}
if (req.url === "/about" && req.method === "GET") {
res.writeHead(200, {
"Content-Type": "text/plain"
});
return res.end("About Page");
}
res.writeHead(404, {
"Content-Type": "text/plain"
});
res.end("Page not found");
});
server.listen(3000);
Now you have basic routing:
/ → Home Page
/about → About Page
other → 404
Frameworks such as Express essentially provide much more sophisticated routing and middleware systems on top of concepts like these.
6. Return HTML
A server doesn’t have to return plain text.
You can send HTML:
const http = require("http");
const server = http.createServer((req, res) => {
res.writeHead(200, {
"Content-Type": "text/html"
});
res.end(`
<!DOCTYPE html>
<html>
<head>
<title>My Server</title>
</head>
<body>
<h1>Welcome</h1>
<p>This page came from my own server.</p>
</body>
</html>
`);
});
server.listen(3000);
The browser receives the HTML and renders it as a webpage.
7. Serve a Static HTML File
Hardcoding HTML inside JavaScript isn’t practical.
Create:
project/
├── server.js
└── index.html
index.html:
<!DOCTYPE html>
<html>
<head>
<title>My Website</title>
</head>
<body>
<h1>Hello World</h1>
<p>Served by my Node.js server.</p>
</body>
</html>
Then use Node’s filesystem module:
const http = require("http");
const fs = require("fs");
const server = http.createServer((req, res) => {
if (req.url === "/") {
fs.readFile("index.html", (err, data) => {
if (err) {
res.writeHead(500);
return res.end("Server Error");
}
res.writeHead(200, {
"Content-Type": "text/html"
});
res.end(data);
});
return;
}
res.writeHead(404);
res.end("Not Found");
});
server.listen(3000, () => {
console.log("Server running on port 3000");
});
Now your server reads the file and delivers it to the browser.
8. Understand HTTP Status Codes
A web server communicates more than just content.
It also tells the client what happened.
Common status codes include:
| Code | Meaning |
|---|---|
200 | OK |
201 | Created |
301 | Moved Permanently |
302 | Found / Redirect |
400 | Bad Request |
401 | Unauthorized |
403 | Forbidden |
404 | Not Found |
500 | Internal Server Error |
503 | Service Unavailable |
For example:
res.writeHead(404);
res.end("Not Found");
tells the browser that the requested resource doesn’t exist.
9. Add an API Endpoint
Your server can also return JSON instead of HTML.
if (req.url === "/api/users") {
const users = [
{
id: 1,
name: "John"
},
{
id: 2,
name: "Jane"
}
];
res.writeHead(200, {
"Content-Type": "application/json"
});
return res.end(JSON.stringify(users));
}
Visiting:
/api/users
could return:
[
{
"id": 1,
"name": "John"
},
{
"id": 2,
"name": "Jane"
}
]
Now your web server is also acting as a simple API server.
10. What Happens When Someone Visits Your Server?
Suppose a user visits:
http://example.com/products
A simplified process is:
1. Browser enters URL
↓
2. DNS resolves example.com
↓
3. Browser connects to server
↓
4. TCP connection established
↓
5. Browser sends HTTP request
↓
6. Server receives request
↓
7. Server determines the route
↓
8. Application processes request
↓
9. Server generates response
↓
10. Browser receives response
↓
11. Browser renders the page
This is the foundation behind the web.
11. From Localhost to the Internet
A server running on:
localhost:3000
is only accessible from your computer.
To make a server publicly accessible, you generally need:
Domain
↓
DNS
↓
Public IP
↓
Firewall / Network
↓
Web Server
↓
Application
↓
Database
A production setup might look like:
Internet
↓
Cloudflare
↓
Nginx
↓
Node.js Application
↓
MySQL / PostgreSQL
Nginx can handle things such as TLS termination, reverse proxying, static files, connection management, and routing traffic to your application.
12. Important Security Considerations
A learning server can be simple. A production server cannot.
You need to consider:
- Input validation
- Authentication and authorization
- HTTPS/TLS
- Rate limiting
- Secure headers
- Error handling
- Logging
- Dependency security
- File-access restrictions
- SQL injection prevention
- Cross-site scripting
- CSRF protection
- Request-size limits
- Secrets management
One particularly important rule is:
Never blindly trust data received from the client.
A request such as:
GET /user?id=123
should not automatically be treated as safe input.
13. What You Are Really Learning
Building a web server from scratch isn’t primarily about creating something that will replace Nginx or Apache.
The real value is understanding what happens underneath web frameworks.
Once you understand:
TCP
↓
HTTP
↓
Request
↓
Routing
↓
Application logic
↓
Response
frameworks become much easier to understand.
Express, Django, Laravel, Spring Boot and other frameworks aren’t magic. They automate and organize many of the repetitive problems involved in handling HTTP requests and building web applications.
A Good Learning Progression
If you want to take the project further, build it in this order:
Basic TCP server
↓
HTTP server
↓
Request parser
↓
Routing
↓
Static files
↓
JSON API
↓
POST requests
↓
Request body parsing
↓
Middleware
↓
Authentication
↓
Database
↓
HTTPS
↓
Reverse proxy
↓
Production deployment
The moment you build even a tiny web server yourself, HTTP stops being something that happens “behind the browser” and becomes something you can actually see and control.

Latest tech news and coding tips.