Contents
TABLE OF CONTENTS
Anshuman Praharaj12 min read

Understanding Routing, Serialization and Deserialization from First Principles

what is routing

in the last blog on http , i described about http and http methods and if understand those things carefully you will understand that http methods show your intent of interaction like what you wanna do like GET means you wanna fetch a resource , POST means you want to create a resource and PUT and PATCH for updating a resource and so on .

Routing as a concept is a extentison of it , routing basically defines where on which resource you wanna work on or get access of .

Routing simply where your intent is headed, where you wanna send the HTTP request.

you are basically telling the server where you want to go.

for example if you hit a route like

GET /user/:id

this basically means you are trying to fetch some speicific users information

/user is the resource , which tells the server that you want user information.

so to sum it up routing is basically mapping url parameters to a server side logic.

Types of Routing

There are different types of routing that a backend engineer works with while designing APIs. Each type of routing serves a different purpose depending on how the client needs to access a resource.

This defines how a resource path is structured and parsed from the client’s perspective. Within this category, there are several common types of routing.

Static Routing

Static routing is the simplest form of routing.

Here, the URL path is completely fixed and is directly attached to a specific handler. There are no dynamic values or variables inside the path.

For example:

GET /api/v1/users

This endpoint can be used to fetch all users available in the application.

Similarly:

GET /api/v1/books

can be used to fetch all the books available in the application.

We generally use this kind of route when the intention is to access a collection of resources.

For example:

/api/v1/users

represents the collection of users, while:

/api/v1/books

represents the collection of books.

The important thing here is that the path itself does not change. The handler attached to /api/v1/users will always handle requests for that exact path.

Dynamic or Parameterized Routing

The next type is dynamic or parameterized routing.

Here, instead of having a completely fixed path, we introduce a variable or path parameter inside the URL. This allows the server to identify and fetch a specific resource dynamically.

For example:

GET /api/v1/users/:id

Here, :id acts as a placeholder for a value that can change depending on the client's request.

For example, the actual requests could look like:

GET /api/v1/users/1
GET /api/v1/users/2
GET /api/v1/users/12
GET /api/v1/users/150

The route itself remains the same:

/api/v1/users/:id

but the value of id keeps changing.

The server then uses that value to determine which resource the client is asking for.

So:

/api/v1/users/1

could mean "give me the user with ID 1", while:

/api/v1/users/150

means "give me the user with ID 150".

This is particularly useful when we want to fetch a specific record rather than an entire collection.

Query-Based Routing

Another common pattern is query-based routing.

In this approach, we use query parameters to pass additional metadata to the server. These parameters appear after the ? in the URL and generally follow a key-value structure.

For example:

GET /api/v1/search?query=backend%20engineering&page=2

Here, query and page are query parameters.

The route itself is still:

/api/v1/search

but the query parameters tell the server how the client wants the data to be processed.

For example:

query=backend engineering
page=2

could mean:

Search for "backend engineering" and return the second page of results.

Query parameters are commonly used for things such as:

  • filtering

  • searching

  • sorting

  • pagination

  • optional configuration

The important distinction here is that query parameters generally don't represent a different resource path. Instead, they provide additional instructions or constraints for how that resource should be retrieved.

For example:

GET /api/v1/books?author=tolkien

could filter books by author, while:

GET /api/v1/books?page=2&limit=20

could control pagination.

This allows us to add optional behavior without creating a completely new URL structure for every possible combination.

Nested Routing

The next type is nested routing.

Nested routes are commonly used in production applications to represent relationships between resources.

The idea is that the URL structure reflects the hierarchy or relationship between resources.

For example:

GET /api/v1/users/:id/posts/:postId

Here, we have two dynamic parameters:

:id
:postId

Suppose the request is:

GET /api/v1/users/42/posts/17

This can be interpreted as:

Find post 17 belonging to user 42.

The route therefore represents a relationship:

User
 └── Posts
      └── Specific Post

This kind of routing can make the relationship between resources explicit from the URL itself.

For example:

/api/v1/users/42/posts

could represent all posts belonging to user 42.

And:

/api/v1/users/42/posts/17

could represent a specific post belonging to that user.

Nested routing becomes particularly useful when resources have a clear parent-child relationship.

However, we also need to be careful not to make routes excessively deep. A URL such as:

/api/v1/users/42/posts/17/comments/8/likes/3

can quickly become difficult to reason about and maintain.

So nested routes are useful when the relationship is meaningful, but they shouldn't be used simply because resources happen to be related somewhere in the database.

Catch-All Routing

The last type in this category is catch-all routing.

A catch-all route acts as a wildcard or fallback route that captures requests that don't match the previously defined routes.

For example, an application might have a route that effectively means:

Anything that doesn't match another route → handle it here

This is commonly useful for implementing custom 404 Not Found handling.

For example, if a client requests:

GET /api/v1/something-that-does-not-exist

and no specific route matches it, a catch-all handler can intercept the request and return a custom response.

Catch-all routes are also commonly used in web applications where the server needs to serve a single frontend entry point for routes that are handled by a client-side router.

For example:

/*

can be used as a fallback that sends requests to a single index.html or application entry point.

The exact syntax for a catch-all route depends on the backend framework, but the underlying idea remains the same: match anything that hasn't already been matched by a more specific route.

So, within URL structure and endpoint routing, we have several useful patterns:

Static
   ↓
/api/v1/users

Dynamic / Parameterized
   ↓
/api/v1/users/:id

Query-Based
   ↓
/api/v1/search?query=backend&page=2

Nested
   ↓
/api/v1/users/:id/posts/:postId

Catch-All
   ↓
/*

Each of these solves a slightly different problem. Static routes give us fixed resource endpoints, parameterized routes let us address specific resources, query parameters add optional filtering or retrieval behavior, nested routes represent relationships between resources, and catch-all routes provide a fallback for unmatched paths.

Serialization and Deserialization

Now that we are done with routing, let's look at another concept in backend engineering that we are going to use everywhere: serialization and deserialization.

In fact, if you look at backend engineering from a very high level, a lot of it boils down to one fundamental problem:

Take data, store it, retrieve it, process it, and give it back to the user.

That's obviously not all that backend engineering is. There are a huge number of engineering patterns, architectures, distributed systems concepts, databases, networking concepts, concurrency problems, caching strategies, and other complexities involved in doing this efficiently and reliably.

But at a very high level, we are constantly dealing with data.

Every API call deals with data. Every database operation deals with data. Every internal service communication deals with data. Every processing utility we write is ultimately operating on some form of data.

This is why understanding how data is represented and transferred is so important.

And this is also why serialization and deserialization are closely related to what we just discussed with routing.

With routing, we talked about how a client accesses or creates a resource on the server. But once the request reaches the server, we need some way of representing the data being sent between the client and the server.

That's where serialization and deserialization come in.

Why Do We Need Serialization?

Let's first understand the problem instead of jumping straight into the definition.

Suppose we have an application with five existing users, and a sixth user is trying to create an account.

Now imagine every client is allowed to send data however they want.

One client says:

"I'll send my name, username, and date of birth as plain text, line by line."

Another client says:

"I'll convert everything into binary and send those bytes to the backend."

Another client says:

"I'll use XML."

Another says:

"I'll use Protocol Buffers."

And another client might use some completely different representation.

The problem should be obvious.

How is our server supposed to know how to interpret all of this?

We could theoretically write separate parsing logic for every possible representation, but that quickly becomes impractical. There are countless ways in which data can be represented and transferred.

Instead, we establish a defined data representation that the communicating parties agree upon.

For example, our API could say:

"When you send user data to this endpoint, send it in this particular structure."

JSON is one of the most commonly used formats for this purpose, but it is important to understand that serialization is not synonymous with JSON.

We could use JSON, XML, Protocol Buffers, MessagePack, a binary protocol, or another format depending on the requirements of the system.

The important part is that both sides understand the representation being used.

What Is Serialization?

Serialization is the process of converting data from its native or in-memory representation into a format that can be stored or transmitted.

Let's say our client has the following user information:

{
  "username": "anshuman",
  "name": "Anshuman",
  "dateOfBirth": "2002-08-15"
}

This is a serialized representation of the user's data in JSON.

The client and server can now agree that user creation requests will follow this structure.

So instead of every client inventing its own representation, we have a predictable format:

Client
   ↓
User data
   ↓
Serialization
   ↓
JSON / Protobuf / XML / Binary format
   ↓
Network
   ↓
Server

This is extremely important in distributed systems because the client and server don't necessarily have to be written in the same programming language.

Your client could be a web browser running JavaScript.

Another client could be an Android application written in Kotlin.

Another could be an iOS application written in Swift.

Another could be a backend service written in Go.

Another could even be an AI agent interacting with your API.

They can all communicate as long as they agree on the format and protocol used to exchange data.

For example, all of them could send a request like:

{
  "username": "anshuman",
  "name": "Anshuman",
  "dateOfBirth": "2002-08-15"
}

The implementation details on each side can be completely different. The important thing is that the wire representation is understood by both sides.

What Is Deserialization?

Now let's look at the other side of the process.

Our server receives the serialized data:

{
  "username": "anshuman",
  "name": "Anshuman",
  "dateOfBirth": "2002-08-15"
}

The server cannot simply assume that this JSON object is already the exact structure that our application needs internally.

Our application might have a native programming-language structure such as:

User
├── username
├── name
└── dateOfBirth

The server therefore needs to parse the incoming representation and convert it into a structure that the application can work with.

This process is called deserialization.

In simple terms:

Deserialization is the process of taking serialized data and converting it back into a representation that the application can understand and process.

So the overall flow looks something like this:

Client

Native data
    ↓
Serialization
    ↓
JSON / Protobuf / XML / Binary
    ↓
Network
    ↓
Deserialization
    ↓
Native application structure

Server

Let's go back to our user creation example.

The client sends:

{
  "username": "anshuman",
  "name": "Anshuman",
  "dateOfBirth": "2002-08-15"
}

The server receives this data and deserializes it into a structure that its application code can work with.

It can then validate the fields, apply business logic, transform the values if necessary, and eventually persist the appropriate values into the database.

For example:

username     → "anshuman"
name         → "Anshuman"
dateOfBirth  → "2002-08-15"

The application can now independently work with these values.

It can validate whether the username already exists, check whether the date of birth is valid, apply business rules, and eventually store the data in the appropriate database columns.

So the database doesn't necessarily care about the JSON representation that travelled over the network.

The JSON was the transport representation.

The application has its own internal representation.

The database has its own representation.

Serialization and deserialization are what allow us to move between these different representations.

The Bigger Picture

This becomes much more interesting when we stop thinking about JSON specifically.

Imagine a request coming from a browser:

JavaScript object
      ↓
    JSON
      ↓
   HTTP
      ↓
   Backend
      ↓
Deserialization
      ↓
Application object

Now imagine communication between two backend services:

Service A
   ↓
Serialize
   ↓
Protocol Buffers
   ↓
Network
   ↓
Service B
   ↓
Deserialize
   ↓
Application object

The underlying concept is the same.

We have some data in one representation, we serialize it into a representation suitable for transmission or storage, and the receiving side deserializes it back into a representation it can work with.

This is why serialization and deserialization are fundamental backend concepts.

Whenever data crosses a boundary, there is usually some form of representation change happening.

That boundary could be:

  • a client and an API

  • one backend service and another

  • an application and a message queue

  • an application and a file

  • an application and a cache

  • an application and a database

The exact format may change, but the underlying problem remains the same:

How do we represent data in a way that another system can reliably understand, transmit, store, and reconstruct?

That's the core idea behind serialization and deserialization.