I am currently working on a project where I have WebSockets, UDP and REST APIs, and although the V1 was almost ready and everything was working, the code had started to feel all over the place.
I didn't always understand where everything was, especially because you don't usually see many projects where multiple protocols like REST, WebSockets and UDP are being used together.
I wanted to reach a point where I could look at the code and immediately understand where things belong.
At the same time, there was a real need to add a new SUPER_ADMIN role and change some of the existing permission levels. So instead of continuing to patch the existing Express application, I decided to rewrite the project in NestJS.
The difference was visible in the structure
In V1, the backend had separate folders for HTTP, services, database, WebSockets and other utilities:
Nothing here is necessarily wrong, but as the project grew, following one feature could mean jumping between several different folders.
With NestJS, I moved toward a more feature-oriented structure:
That small change made a big difference for me. I could look at the project and have a much better idea of where something belongs.
Starting with the database
The first thing I worked on was the database.
I redesigned parts of the schema and moved the main database access to Prisma. What I liked about Prisma was that the schema became much easier to understand and the database models were connected more naturally with TypeScript.
model User {
id String @id @default(uuid())
email String @unique
role UserRole
}
I didn't completely remove raw SQL either. There are still places where SQL makes more sense, especially when I need database-specific functionality or more direct control.
Rebuilding the APIs
After that, I started rewriting the APIs.
In the old Express version, it was very easy for routes to slowly become responsible for validation, business logic, database calls and responses all in one place.
With NestJS, I started separating these responsibilities:
@Controller("users")
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
create(@Body() dto: CreateUserDto) {
return this.usersService.create(dto);
}
}
The controller handles the request, the service handles the logic, and the database layer handles persistence.
NestJS's dependency injection also meant I could declare what a class needs instead of manually creating and passing everything around.
Authorization
Then I rebuilt the authorization because of the new SUPER_ADMIN role and the permission changes.
NestJS's guards and decorators made it possible to make permissions much more visible:
@Roles("ADMIN")
@Delete(":id")
remove(@Param("id") id: string) {
return this.usersService.remove(id);
}
I could look at a route and understand who is allowed to access it without searching through different parts of the application.
WebSockets
Next came the WebSocket side.
NestJS provides WebSocket gateway support with Socket.IO, so I could keep the real-time communication in its own place.
For example, rooms could be handled inside the gateway:
@WebSocketGateway()
export class RealtimeGateway {
@WebSocketServer()
server: Server;
@SubscribeMessage("join-room")
joinRoom(
@ConnectedSocket() client: Socket,
@MessageBody() data: { room: string },
) {
client.join(data.room);
}
}
Now the real-time communication has a clear place, while the actual business logic can stay in services.
What NestJS gave me was a clear place to isolate it from the rest of the application and manage its lifecycle.
So the mental model became:
REST → Controllers → Services → Database
WebSocket → Gateway → Rooms → Services
UDP → Transport → Services
I also used Bruno while rebuilding the APIs. I found it to be a great alternative to Postman, especially for keeping API requests and collections close to the project while rebuilding and testing the endpoints.
The biggest benefit
The biggest benefit I got from the rewrite wasn't performance.
It was that I could finally understand the project in layers.
I could look at the code and know where the API lives, where the business logic lives, where WebSockets are handled, where UDP is handled, and where permissions belong.
And this is probably what I like most about opinionated frameworks.
We don't have to decide between 10 or 20 different ways of structuring everything. NestJS gives us conventions, and we can follow them.
That doesn't mean NestJS is universally better than Express. Express was perfectly capable of running my original project.
For me, the rewrite was about making the architecture predictable and easier to understand.
Sometimes the best reason to rewrite something isn't that the old version doesn't work.
It's that you want the next version to be easier to understand.

Top comments (0)