Application Layers and Request Flow
Before you start
You need: nothing technical. This article explains the shape of a web application from scratch, and every later track builds on it.
Time: about 45 minutes, plus the practice.
New to all of this? Start here explains what an application is made of and introduces the school system every example uses. Any word you do not recognise is in the glossary.
Learning objective
Trace one request from a browser click to a database row and back, and say what each layer is responsible for.
Topics
- Why software is built in layers
- The five layers
- Request and response
- HTTP methods and status codes
- Payloads and JSON
- A complete flow — searching for a student
- Authentication and authorisation
- Which layer owns a failure
Why layers
The school portal could be written as one file that reads the keyboard, calculates fees and writes to disk. Nobody does this, for four reasons:
| Reason | Consequence of no layers |
|---|---|
| Change | Redesigning the page means touching the fee calculation |
| Reuse | The mobile app cannot use logic welded into a web page |
| Testing | Fee logic can only be tested by clicking through a browser |
| Specialisation | Nobody can work on the UI without understanding SQL |
Each layer knows only about the one beneath it. The browser knows there is an API; it does not know there is SQL Server. Swap SQL Server for PostgreSQL and the browser code does not change.
The five layers
Browser (Presentation)
│ what the user sees and clicks
▼
API (Controller)
│ receives requests, validates, returns responses
▼
Service (Business logic)
│ the rules — what a fee balance means, who may see what
▼
Repository (Data access)
│ translates between objects and SQL
▼
Database
stores the data
| Layer | Responsible for | Never does |
|---|---|---|
| Browser | Display, input, client-side convenience checks | Enforce security, calculate fees |
| API | Routing, validation, status codes, serialisation | Contain business rules or SQL |
| Service | Business rules, orchestration, authorisation decisions | Know about HTTP or SQL syntax |
| Repository | Queries, mapping rows to objects | Contain business rules |
| Database | Storage, constraints, integrity | Contain application logic (mostly) |
The most common structural mistake is business logic in the wrong layer.
Fee balance calculated in the browser → the mobile app gets a different answer
Fee balance calculated in the API → cannot be tested without HTTP
Fee balance calculated in the service → correct; one place, testable
Every layer boundary is also a debugging boundary. When the fee page shows the wrong number, you ask at each boundary: was the data correct here? The first boundary where the answer is no contains the bug. Track 17 turns this into a method.
Request and response
Everything a browser does is a request and a response. One goes out, one comes back.
REQUEST
GET /api/students/search?query=NCA-2024-0012 HTTP/1.1
Host: api.nexcoding.in
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Accept: application/json
RESPONSE
HTTP/1.1 200 OK
Content-Type: application/json
{
"students": [
{
"publicId": "8f14e45f-ceea-467a-9a1c-2b1c3f4d5e6a",
"name": "Ravi Kumar",
"rollNumber": "NCA-2024-0012",
"className": "10th",
"section": "A"
}
],
"totalCount": 1
}
| Part | Purpose |
|---|---|
| Method | GET — what kind of operation |
| Path | /api/students/search — which resource |
| Query string | ?query=... — parameters |
| Headers | Authorisation, content type, caching |
| Body (payload) | Data sent with the request; GET has none |
| Status code | 200 OK — what happened |
HTTP is stateless. The server remembers nothing between requests. That is why the Authorization header is on every single one — the server has no memory that you signed in a minute ago.
HTTP methods
| Method | Means | School example | Safe to repeat? |
|---|---|---|---|
GET | Read | Fetch the student list | Yes — changes nothing |
POST | Create | Record a fee payment | No — twice creates two payments |
PUT | Replace | Update a whole student record | Yes — same result each time |
PATCH | Partially update | Change only the section | Yes |
DELETE | Remove | Deactivate a staff member | Yes |
GET must never change anything. A browser or proxy may fetch a GET URL speculatively; if GET /api/students/12/delete deletes a student, something will eventually delete one for you.
POST repeated creates duplicates. A user double-clicking "Record payment" can record ₹12,000 twice. This is a real problem with real money, and the API track covers guarding against it.
Status codes
| Code | Means | Whose problem |
|---|---|---|
| 200 OK | Success | — |
| 201 Created | Created; usually returns the new record | — |
| 204 No Content | Success, nothing to return | — |
| 400 Bad Request | The request was malformed or invalid | Caller — read the response body |
| 401 Unauthorized | Not signed in, or the token expired | Caller — authenticate |
| 403 Forbidden | Signed in, not allowed | Caller — permissions |
| 404 Not Found | No such resource | Caller — wrong URL or id |
| 409 Conflict | Clashes with current state, e.g. duplicate roll number | Caller |
| 500 Internal Server Error | The server crashed | Server |
| 503 Service Unavailable | The server is down or overloaded | Server |
The first digit tells you who to talk to. 4xx means the caller sent something wrong; 5xx means the server failed. A frontend developer chasing a 500 is chasing a backend bug.
401 and 403 are different and constantly confused. 401 is "I do not know who you are"; 403 is "I know, and you may not". A teacher getting 403 on the salary report is the system working correctly.
Payloads and JSON
The payload is the data carried in the body of a request or response.
POST /api/fee-payments
Content-Type: application/json
{
"studentPublicId": "8f14e45f-ceea-467a-9a1c-2b1c3f4d5e6a",
"amount": 12000.00,
"paymentMode": "Online",
"transactionId": "TXN-884213",
"paidOn": "2026-08-27"
}
Note what is not in that payload: schoolId. The server takes it from the signed-in user's token, never from the request — because a caller who can send schoolId can send someone else's.
JSON is the standard format: text, human-readable, supported everywhere.
{
"name": "Ravi Kumar",
"marksObtained": null,
"isAbsent": true,
"totalFees": 15000.00,
"subjects": ["Maths", "Science"]
}
| Field | JSON type |
|---|---|
name | string |
marksObtained | null — absent, not zero |
isAbsent | boolean |
totalFees | number |
subjects | array |
JSON has no comments. You cannot annotate it the way you would code — a // line makes the whole document invalid. That catches people out most often in appsettings.json and package.json.
"marksObtained": null is how an absent student travels between layers. Sending 0 instead means the browser cannot tell an absence from a zero score, and every average computed downstream is wrong.
Content-Type: application/json must be sent on any request with a JSON body. Without it the server returns 415 Unsupported Media Type, and the message rarely says which header is missing.
Example flow: searching for a student
Priya Sharma, office administrator, types NCA-2024-0012 and presses Enter.
1. Browser
const response = await fetch(`/api/students/search?query=${encodeURIComponent(query)}`, {
headers: { "Authorization": `Bearer ${token}` }
});
const data = await response.json();
The browser sends the request with the token. It does not send a school id — it does not decide which school's data it may see.
2. API
[Authorize]
[HttpGet("api/students/search")]
public async Task<IActionResult> Search([FromQuery] string query)
{
if (string.IsNullOrWhiteSpace(query))
{
return BadRequest(new { message = "A search term is required." });
}
int schoolId = int.Parse(User.FindFirst("schoolId").Value);
List<StudentSummaryDto> students = await _studentService.SearchAsync(schoolId, query);
return Ok(new { students, totalCount = students.Count });
}
Validates the input, reads schoolId from the token, calls the service, returns a status code. No SQL, no business rules.
3. Service
public async Task<List<StudentSummaryDto>> SearchAsync(int schoolId, string query)
{
string trimmed = query.Trim();
List<Student> students = await _repository.SearchAsync(schoolId, trimmed);
return students
.Where(s => s.Status == StudentStatus.Active)
.OrderBy(s => s.RollNumber)
.Select(StudentSummaryDto.From)
.ToList();
}
Applies the rules: trim the input, exclude inactive students, order by roll number, and project into a summary that omits ParentPhone.
4. Repository
public async Task<List<Student>> SearchAsync(int schoolId, string query)
{
const string sql = @"SELECT Id, PublicId, Name, RollNumber, ClassName, Section, Status
FROM Student
WHERE SchoolId = @SchoolId
AND IsDeleted = 0
AND (RollNumber = @Query OR Name LIKE @NameQuery)";
// parameters supplied separately, never concatenated
}
5. Database returns the matching row. The response travels back up: rows become objects, objects become a DTO, the DTO becomes JSON, the browser renders a table.
Five layers, one row, and SchoolId filtered at the only layer that touches data.
Authentication and authorisation
| Question | Failure | |
|---|---|---|
| Authentication | Who are you? | 401 |
| Authorisation | What may you do? | 403 |
Authentication first, then authorisation. Signing in proves identity; permissions decide what that identity may do.
In the School system:
| Role | May |
|---|---|
| SuperAdmin | Everything, across schools |
| Admin | Everything within their school |
| Principal | View everything in their school; approve changes |
| Teacher | Their subjects' marks and attendance |
| Staff | Their department's records |
| Student | Their own record only |
Every one of these checks happens on the server. Hiding a button in the browser is a courtesy to the user, not a control — anyone can call the endpoint directly with Postman.
And the tenant boundary is separate from the role. An Admin of School 1 has full rights within School 1. schoolId comes from the token on every request; a Principal cannot see School 2 by changing a URL.
Which layer owns a failure
| Symptom | Layer |
|---|---|
| No request appears in the browser's Network tab | Browser — the handler never fired |
| Request sent, 200, correct data, wrong display | Browser — rendering |
| Request sent, 200, wrong data | Service or repository |
| 400 | Browser sent something invalid — read the response body |
| 401 | Token missing or expired |
| 403 | Authorisation — often correct behaviour |
| 404 | Wrong URL, or the record does not exist |
| 500 | Server — an unhandled exception, in the API log |
| Data from the wrong school | Repository — missing SchoolId filter |
One check settles most frontend-versus-backend arguments: open the Network tab, find the request, read the status code and the response body. Track 17 builds the full method.
Where this goes wrong
| The mistake | Consequence |
|---|---|
| Business logic in the browser | The mobile app gets a different answer |
| Trusting client-side validation | Postman bypasses it entirely |
Taking schoolId from the request | One school reads another's records, with a 200 response |
GET requests that change data | A proxy or a crawler eventually deletes something |
| Confusing 401 with 403 | Chasing a login bug that is really a permissions one |
| Treating a 500 as a frontend problem | Hours lost in the wrong codebase |
| Hiding a button and calling it security | The endpoint is still reachable |
Every one of these is a layer doing a job that belongs to another layer. The five-layer split is not architecture theory; it is what stops these happening.
Common mistakes
- Business logic in the browser
- SQL in the controller
- Trusting client-side validation as a control
- Taking
schoolIdfrom the request instead of the token GETrequests that change data- Not guarding against a repeated
POST - Sending
0for an absent student instead ofnull - Omitting
Content-Type: application/jsonand puzzling over 415 - Confusing 401 with 403
- Treating a 500 as a frontend problem
- Hiding a button and calling it security
Practice
The course exercise is label a three-tier application diagram.
- Draw the five layers and write one sentence on what each is responsible for.
- For each layer, write one thing it must never do.
- Open any website, open DevTools → Network, reload, and find one
GETrequest. Read its method, path, headers and status. - Find a request that returns JSON and read the response body.
- Trigger a 404 by editing a URL, and a 401 by clearing your session.
- For each status code in the table, write which layer you would investigate.
- Trace the student-search flow through all five layers, and say at which layer the
SchoolIdfilter is applied and where the value came from. - Explain what would break if
schoolIdwere taken from the query string instead. - Write the JSON payload for recording a ₹12,000 online fee payment. Confirm you did not include
schoolId. - Write the JSON for an absent exam result. Confirm
marksObtainedisnull, not0. - Explain what a mobile app would have to duplicate if the fee balance were calculated in the browser.
- For each of these, name the layer at fault: the page is blank; the API returns another school's students; the search box does nothing when clicked; every call returns 500.
Exercise 7 is the one to be able to narrate out loud — it is a standard interview question.
You can now
- Name the five layers and what each is responsible for
- Trace one request from a browser click to a database row and back
- Read an HTTP method, status code and payload correctly
- Tell 401 from 403
- Say why
schoolIdmust come from the token - Decide from a status code which layer owns a failure
Review questions
- What does each layer know about, and what does it deliberately not know?
- Why must
schoolIdcome from the token rather than the request? - What is the difference between 401 and 403?
- Why is hiding a button in the browser not a security control?
Next: Environments and SDLC