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.
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.
A basic web server needs a few fundamental components:
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.
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.
This line creates the server:
http.createServer() The callback receives two important objects:
(req, res) reqreq 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 resres represents the response you send back to the client.
For example:
res.writeHead(200);
res.end("Hello!"); The 200 means:
OK 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.
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.
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.
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.
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.
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.
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.
A learning server can be simple. A production server cannot.
You need to consider:
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.
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.
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.
A shell prompt is the text displayed by a command-line shell to show that it is ready…
A SOC Analyst (Security Operations Center Analyst) is one of the frontline defenders of an organization’s cybersecurity…
Most image upload systems assume one thing: the user has a stable internet connection. That assumption…
Imagine a user submits a form while their internet connection suddenly disappears. Normally, the request…
Cybercriminals don't always announce their presence. Many compromises are designed to remain unnoticed for weeks…
For years, jQuery was everywhere. If you were building websites in the 2010s, there was a…