DEV Community

Qtim
Qtim

Posted on

NestJS vs Go: Lessons From Production Projects

Four production systems, 18 user roles, around 460 candidates, deadline queues, and video lessons for up to 100 participants — what made NestJS a fit, and where Qtim introduced Go.

A student opens one lesson. The product checks their role, loads the schedule, shows the right assignments, accepts a file, and starts a video session.

For the student, this is one interface. For the backend, it is a set of different jobs: access control, state transitions, background processing, reports, integrations, and a live media path.

Qtim has used NestJS across several education systems because most of that work is shaped by business rules that keep changing after launch. Go entered the architecture in one specific subsystem: video, where WebRTC, an SFU, media routing, and latency created a different workload.

The takeaway from production is less dramatic than a “NestJS versus Go” debate. A stack decision starts with the part of the system that changes most often and the part that carries the heaviest load.

Eighteen roles made access control the product

Qtim joined the Online School №1 project after the client had struggled with previous contractors. The first release had to go live within three months without closing the door on later changes.

Students saw lessons, homework, materials, teacher chat, calendars, and progress. Behind that interface were more than ten subprojects and 18 user types, including teachers, curators, coordinators, and administrators. Each role needed its own view of groups, schedules, assignments, and project settings.

The team separated those rules with NestJS modules, services, guards, and policies. A guard checked whether a user could enter a scenario; a policy described what that user could do with a particular object. CASL supported the more detailed rules, such as giving a curator access to one student group while allowing a coordinator to manage schedules.

That structure made access changes predictable. A new permission could be configured in 15–20 minutes instead of being threaded through the entire backend. Assignments, attempts, calendars, and schedules later moved behind clearer service boundaries, with RabbitMQ carrying events away from the main user flow.

NestJS worked here because the difficult part was changeability. New roles and workflows had to fit without weakening the access model.

Around 460 candidates made every status transition consequential

The university attestation platform brought registration, document checks, exams, video evidence, assessment, protocols, reports, and federal-registry exports into one process.

A status error could break the chain. The candidate needed to move from registration to document verification, the exam, grading, and final records in the correct order. Administrators, examiners, and super administrators required different actions and different access to media.

Qtim kept status transitions in the service layer rather than controllers. The controller accepted an action; the service checked whether the application could move forward; guards blocked unauthorized media access before a request reached the handler.

Around 460 candidates passed through the platform. The value of the architecture was the ability to keep each status, document, recording, and permission inside one testable process. Raw request throughput was secondary to preserving the rules.

The deadline peak belonged in a queue

StudyCats needed a complete preparation system for Russian school exams: course pages, schedules, homework, mock tests, revision cards, statistics, and gamification. Qtim delivered it with NestJS and Nuxt.js in five months.

Education platforms have a predictable bad habit: many students submit work just before a deadline. File validation and audio conversion then compete with the learning interface at the exact moment that interface is busiest.

The heavy operations moved to BullMQ queues backed by Redis. A student submitted an assignment and returned to the lesson. A separate worker processed the files, ran the required checks, and updated the result status. Voice recordings followed the same path: conversion happened in the background, while the player received a consistent format across devices.

The queue protected the user journey without introducing another programming language. That distinction matters. A workload problem can often be isolated inside the current stack before a team adds a second runtime.

A 100-person video lesson created a second workload

The video-conferencing product for Online School №1 had a different profile. Lessons could include up to 100 participants while schedules, authorization, recordings, teacher controls, and the connection to the learning platform still had to work as one experience.

Qtim kept the API, administration tools, roles, and schedules in NestJS. The media subsystem used WebRTC and Go.

Node.js handles large volumes of concurrent I/O well. The media path brought CPU-heavy work: packet processing, SFU routing, fan-out to many participants, active connections, and latency control. That boundary justified a separate service with its own runtime.

The result was a mixed architecture with a clear division of responsibility. NestJS owned the product rules. Go handled the connection-heavy media path. Users still saw one lesson interface.

This case does not turn Go into the default choice for WebRTC. It shows the condition that made a second language reasonable: a bounded subsystem had a distinct workload and could be operated independently.

The second stack comes with a permanent bill

NestJS modules earn their keep in a large product with many roles and changing workflows. In a small service, the same structure can add ceremony where one handler would have been enough.

Microservices create independent boundaries and also introduce distributed debugging, message delivery, data consistency, and more operational surfaces. The online-school platform accepted that cost where assignments, attempts, calendars, and schedules needed more autonomy.

Go also adds a runtime, build process, deployment path, monitoring setup, and a boundary between product logic and the media subsystem. The performance gain has to cover those continuing costs. Otherwise, the team has created a second stack and kept the original problem.

Choose for the next production change

NestJS fits a product dominated by roles, workflows, documents, payments, reports, and integrations. Its value grows when the team expects those rules to change after launch.

A separate Go service becomes reasonable when production evidence points to a different workload, the hot path has a stable boundary, and the expected gain covers the cost of operating another stack.

The decision rule is straightforward: keep changing business logic in the application layer; isolate a second runtime only around a measured workload that can stand on its own.

If a product combines complex roles, background processing, integrations, or load-sensitive services, the Qtim team can map the boundaries and plan the next production architecture.

Top comments (0)