Skip to main content
Published / updated

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:

ReasonConsequence of no layers
ChangeRedesigning the page means touching the fee calculation
ReuseThe mobile app cannot use logic welded into a web page
TestingFee logic can only be tested by clicking through a browser
SpecialisationNobody 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
LayerResponsible forNever does
BrowserDisplay, input, client-side convenience checksEnforce security, calculate fees
APIRouting, validation, status codes, serialisationContain business rules or SQL
ServiceBusiness rules, orchestration, authorisation decisionsKnow about HTTP or SQL syntax
RepositoryQueries, mapping rows to objectsContain business rules
DatabaseStorage, constraints, integrityContain 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
}
PartPurpose
MethodGET — what kind of operation
Path/api/students/search — which resource
Query string?query=... — parameters
HeadersAuthorisation, content type, caching
Body (payload)Data sent with the request; GET has none
Status code200 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

MethodMeansSchool exampleSafe to repeat?
GETReadFetch the student listYes — changes nothing
POSTCreateRecord a fee paymentNo — twice creates two payments
PUTReplaceUpdate a whole student recordYes — same result each time
PATCHPartially updateChange only the sectionYes
DELETERemoveDeactivate a staff memberYes

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

CodeMeansWhose problem
200 OKSuccess
201 CreatedCreated; usually returns the new record
204 No ContentSuccess, nothing to return
400 Bad RequestThe request was malformed or invalidCaller — read the response body
401 UnauthorizedNot signed in, or the token expiredCaller — authenticate
403 ForbiddenSigned in, not allowedCaller — permissions
404 Not FoundNo such resourceCaller — wrong URL or id
409 ConflictClashes with current state, e.g. duplicate roll numberCaller
500 Internal Server ErrorThe server crashedServer
503 Service UnavailableThe server is down or overloadedServer

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"]
}
FieldJSON type
namestring
marksObtainednull — absent, not zero
isAbsentboolean
totalFeesnumber
subjectsarray

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

QuestionFailure
AuthenticationWho are you?401
AuthorisationWhat may you do?403

Authentication first, then authorisation. Signing in proves identity; permissions decide what that identity may do.

In the School system:

RoleMay
SuperAdminEverything, across schools
AdminEverything within their school
PrincipalView everything in their school; approve changes
TeacherTheir subjects' marks and attendance
StaffTheir department's records
StudentTheir 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

SymptomLayer
No request appears in the browser's Network tabBrowser — the handler never fired
Request sent, 200, correct data, wrong displayBrowser — rendering
Request sent, 200, wrong dataService or repository
400Browser sent something invalid — read the response body
401Token missing or expired
403Authorisation — often correct behaviour
404Wrong URL, or the record does not exist
500Server — an unhandled exception, in the API log
Data from the wrong schoolRepository — 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 mistakeConsequence
Business logic in the browserThe mobile app gets a different answer
Trusting client-side validationPostman bypasses it entirely
Taking schoolId from the requestOne school reads another's records, with a 200 response
GET requests that change dataA proxy or a crawler eventually deletes something
Confusing 401 with 403Chasing a login bug that is really a permissions one
Treating a 500 as a frontend problemHours lost in the wrong codebase
Hiding a button and calling it securityThe 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 schoolId from the request instead of the token
  • GET requests that change data
  • Not guarding against a repeated POST
  • Sending 0 for an absent student instead of null
  • Omitting Content-Type: application/json and 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.

  1. Draw the five layers and write one sentence on what each is responsible for.
  2. For each layer, write one thing it must never do.
  3. Open any website, open DevTools → Network, reload, and find one GET request. Read its method, path, headers and status.
  4. Find a request that returns JSON and read the response body.
  5. Trigger a 404 by editing a URL, and a 401 by clearing your session.
  6. For each status code in the table, write which layer you would investigate.
  7. Trace the student-search flow through all five layers, and say at which layer the SchoolId filter is applied and where the value came from.
  8. Explain what would break if schoolId were taken from the query string instead.
  9. Write the JSON payload for recording a ₹12,000 online fee payment. Confirm you did not include schoolId.
  10. Write the JSON for an absent exam result. Confirm marksObtained is null, not 0.
  11. Explain what a mobile app would have to duplicate if the fee balance were calculated in the browser.
  12. 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 schoolId must come from the token
  • Decide from a status code which layer owns a failure

Review questions

  1. What does each layer know about, and what does it deliberately not know?
  2. Why must schoolId come from the token rather than the request?
  3. What is the difference between 401 and 403?
  4. Why is hiding a button in the browser not a security control?

Next: Environments and SDLC