<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Gagan Kumar</title>
    <description>The latest articles on Forem by Gagan Kumar (@ggn_dev).</description>
    <link>https://forem.com/ggn_dev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3812576%2F74b2d729-5861-4037-bb54-24612b006e81.jpg</url>
      <title>Forem: Gagan Kumar</title>
      <link>https://forem.com/ggn_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/ggn_dev"/>
    <language>en</language>
    <item>
      <title>Laravel vs Node.js: Which Backend Should You Choose in 2026?</title>
      <dc:creator>Gagan Kumar</dc:creator>
      <pubDate>Sun, 08 Mar 2026 07:42:13 +0000</pubDate>
      <link>https://forem.com/ggn_dev/laravel-vs-nodejs-which-backend-should-you-choose-in-2026-5d84</link>
      <guid>https://forem.com/ggn_dev/laravel-vs-nodejs-which-backend-should-you-choose-in-2026-5d84</guid>
      <description>&lt;p&gt;Every developer eventually hits this crossroads. You've got a new project coming up — maybe a REST API, maybe a full web app — and someone asks: &lt;em&gt;"So what are we building this in?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you're comfortable with both PHP and JavaScript, the answer isn't obvious. Laravel and Node.js are both genuinely excellent. Both have massive ecosystems. Both can handle production-grade applications at scale. Picking the wrong one won't ruin your project, but picking the right one will make your life significantly easier.&lt;/p&gt;

&lt;p&gt;I've built real projects with both. &lt;a href="https://gagankumar.me" rel="noopener noreferrer"&gt;Skills360.ai&lt;/a&gt; — a job-seeking platform — runs on Laravel. &lt;a href="https://typeracer.gagankumar.me" rel="noopener noreferrer"&gt;TypeRacrer&lt;/a&gt; — a real-time multiplayer typing game — runs on Node.js with Express and Socket.IO. Same developer, different tools, completely different reasons.&lt;/p&gt;

&lt;p&gt;Here's everything I wish someone had told me before I had to figure it out myself.&lt;/p&gt;




&lt;h2&gt;
  
  
  First, Let's Be Clear About What We're Actually Comparing
&lt;/h2&gt;

&lt;p&gt;This is important because people often compare the wrong things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Laravel&lt;/strong&gt; is a full-featured PHP framework. It comes with everything — routing, ORM, authentication, queues, scheduling, validation, templating — out of the box. It's opinionated, meaning it has a preferred way of doing almost everything, and that's by design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Node.js&lt;/strong&gt; is a JavaScript runtime, not a framework. When people say "Node.js backend," they usually mean Node.js plus Express (or Fastify, or Hono) plus whatever libraries they've assembled around it. You're building your own stack. The flexibility is intentional, but so is the responsibility that comes with it.&lt;/p&gt;

&lt;p&gt;This distinction matters. Comparing Laravel to Node.js is a little like comparing a fully furnished apartment to an empty one with great bones. Both can become a great home — but your starting point is very different.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Developer Experience Gap Is Real
&lt;/h2&gt;

&lt;p&gt;Let me be direct: &lt;strong&gt;Laravel's developer experience is exceptional in a way that's hard to overstate.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You install it and you immediately have routing, an ORM, authentication scaffolding, form validation, a testing suite, a CLI tool (Artisan), and database migrations — all working together, all following consistent conventions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Scaffold a full authentication system in one command&lt;/span&gt;
php artisan make:auth

&lt;span class="c"&gt;# Generate a model, migration, controller and resource routes together&lt;/span&gt;
php artisan make:model Post &lt;span class="nt"&gt;-mcr&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That second command creates your database migration, your Eloquent model, and your controller with all RESTful methods stubbed out — in one go. There's a reason Laravel developers talk about it the way they do.&lt;/p&gt;

&lt;p&gt;Node.js with Express is the opposite experience. You start with almost nothing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From here, every decision is yours. Authentication? Pick a library. Validation? Pick a library. ORM or query builder? Pick one. Sessions? You guessed it. For an experienced developer who knows exactly what they want, this is liberating. For someone who just wants to build a feature, it's a lot of yak shaving before you write a single line of business logic.&lt;/p&gt;

&lt;p&gt;This isn't a criticism of Node.js — it's just the honest trade-off. You get maximum flexibility in exchange for doing more setup work yourself.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Node.js Has a Genuine, Undeniable Edge
&lt;/h2&gt;

&lt;p&gt;There's one area where Node.js doesn't just compete with Laravel — it fundamentally changes what's possible: &lt;strong&gt;real-time, event-driven applications.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Node.js is built on a non-blocking, event-driven architecture. It handles thousands of concurrent connections efficiently without spawning a new thread for each one. For most web apps this doesn't matter much. But for anything involving live updates — chat apps, multiplayer games, collaborative tools, live dashboards — it's a massive architectural advantage.&lt;/p&gt;

&lt;p&gt;This is exactly why I chose Node.js for TypeRacrer. When you have multiple players typing simultaneously and every keystroke needs to be broadcast to all other players in milliseconds, you want a runtime that was designed for this. Socket.IO runs natively on Node.js, and the event-driven model maps perfectly to the event-driven nature of a real-time game.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Node.js handles thousands of concurrent socket connections naturally&lt;/span&gt;
&lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;connection&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;typing-progress&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Broadcast to everyone else in the room instantly&lt;/span&gt;
    &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;player-update&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Could you do real-time with Laravel? Yes — Laravel has WebSockets support via Laravel Reverb and Laravel Echo. But it's an add-on to a framework that wasn't designed around it. With Node.js, you're working with the grain of the runtime rather than against it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Laravel Has a Genuine, Undeniable Edge
&lt;/h2&gt;

&lt;p&gt;Laravel's superpower is &lt;strong&gt;structured application development at speed.&lt;/strong&gt; When you're building something with a database, user accounts, business logic, and a REST API — the typical web application — Laravel gives you a productivity advantage that's hard to match.&lt;/p&gt;

&lt;p&gt;Eloquent, Laravel's ORM, is particularly good. Relationships between models feel natural and readable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Get a user's job applications with their related job postings&lt;/span&gt;
&lt;span class="nv"&gt;$applications&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'applications.jobPosting'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Scope complex queries cleanly&lt;/span&gt;
&lt;span class="nv"&gt;$activeJobs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JobPosting&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;active&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;postedThisWeek&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare this to writing raw SQL or using a query builder from scratch in Node.js, and you'll feel the difference immediately. Eloquent lets you think at the business logic level rather than the database query level.&lt;/p&gt;

&lt;p&gt;The same is true for validation, which in Laravel is almost enjoyable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="s1"&gt;'email'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'required|email|unique:users'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'password'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'required|min:8|confirmed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'resume'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'nullable|file|mimes:pdf|max:2048'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One array. No library to install. No schema to define separately. Just rules, written in plain English, that automatically return the right HTTP response if they fail.&lt;/p&gt;

&lt;p&gt;For Skills360.ai — a job board with employer and candidate accounts, job postings, applications, file uploads, and search — Laravel was the right call. The structure it enforces kept the codebase organized as it grew, and the built-in tooling meant I spent my time on product features rather than plumbing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Performance: The Conversation People Have the Wrong Way
&lt;/h2&gt;

&lt;p&gt;This comes up constantly and it's usually framed incorrectly.&lt;/p&gt;

&lt;p&gt;Node.js is faster at handling many concurrent I/O-bound connections — things like network requests, file reads, and database queries happening simultaneously. Its non-blocking model shines here.&lt;/p&gt;

&lt;p&gt;PHP/Laravel is faster at CPU-bound tasks in some benchmarks, and modern PHP (especially PHP 8.x with JIT compilation) is significantly faster than PHP's reputation suggests.&lt;/p&gt;

&lt;p&gt;But here's the honest truth: &lt;strong&gt;for the vast majority of web applications, neither is the bottleneck.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your database queries are almost always the bottleneck. Your server's memory. Your network latency. Choosing Node.js over Laravel for "performance" reasons — for a standard web app — is like buying a Formula 1 car because you want to get to the supermarket faster. The limiting factor isn't the car.&lt;/p&gt;

&lt;p&gt;Where performance genuinely matters — high-concurrency real-time systems, millions of requests per second — you'll be thinking about infrastructure, caching, and architecture long before you're thinking about which language is faster.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Ecosystem Argument
&lt;/h2&gt;

&lt;p&gt;Both have mature ecosystems. But they're mature in different ways.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;npm&lt;/strong&gt; (Node.js's package ecosystem) is the largest software registry in the world. There is a package for almost everything. The downside is quality control — npm's openness means you also find abandoned, poorly maintained, or insecure packages easily. Choosing the right dependencies requires judgment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Composer&lt;/strong&gt; (PHP/Laravel's package manager) is smaller but more curated. The Laravel ecosystem in particular — Cashier for payments, Sanctum for API auth, Horizon for queue monitoring, Telescope for debugging — is exceptionally well-integrated. These aren't third-party bolt-ons; they're first-party tools built by the same team with the same conventions.&lt;/p&gt;

&lt;p&gt;If you want a rich, carefully curated ecosystem that works together seamlessly: Laravel wins. If you want maximum options and are comfortable vetting packages yourself: Node.js wins.&lt;/p&gt;




&lt;h2&gt;
  
  
  TypeScript Changes the Node.js Equation
&lt;/h2&gt;

&lt;p&gt;One thing that shifted my view of Node.js significantly: TypeScript.&lt;/p&gt;

&lt;p&gt;Plain JavaScript on the backend has real problems. Large codebases become hard to maintain. Refactoring is risky. The lack of types means bugs that TypeScript would catch at compile time instead blow up in production.&lt;/p&gt;

&lt;p&gt;TypeScript solves this. With TypeScript, you get type safety, better IDE support, and the ability to share type definitions between your frontend and backend — something I took full advantage of in TypeRacrer's monorepo setup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Shared types used by both Express server and React client&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Player&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;wpm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;finished&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Laravel has always had the equivalent of this — PHP's type system, combined with IDE plugins like PHPStan, gives you strong typing. But TypeScript on Node.js brings JavaScript much closer to that level of safety, which makes it a more serious option for large-scale applications than it used to be.&lt;/p&gt;




&lt;h2&gt;
  
  
  So, Which Should You Choose?
&lt;/h2&gt;

&lt;p&gt;Here's my honest, opinionated answer:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose Laravel if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're building a standard web application or REST API with a database&lt;/li&gt;
&lt;li&gt;You want to move fast with strong conventions guiding you&lt;/li&gt;
&lt;li&gt;Your team includes developers more comfortable with PHP&lt;/li&gt;
&lt;li&gt;You need a rich built-in feature set without assembling a stack yourself&lt;/li&gt;
&lt;li&gt;The project is content-heavy, e-commerce, or business logic-intensive&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Choose Node.js if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're building something real-time — chat, live collaboration, multiplayer, streaming&lt;/li&gt;
&lt;li&gt;Your team is already deep in the JavaScript ecosystem&lt;/li&gt;
&lt;li&gt;You're sharing code between a React/Vue frontend and your backend (monorepo)&lt;/li&gt;
&lt;li&gt;You need maximum control over your stack architecture&lt;/li&gt;
&lt;li&gt;You're building microservices and want one language across all of them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The one thing I'd push back on:&lt;/strong&gt; the idea that you have to pick one forever. I use both. The right tool depends on the project, not on developer loyalty. A developer who can reach for Laravel when a project calls for it and Node.js when something else does is more valuable than one who insists everything must be built in their favourite stack.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Honest Summary
&lt;/h2&gt;

&lt;p&gt;Laravel makes you productive immediately and keeps your code organized as projects grow. It's the better default choice for most web applications — not because Node.js is bad, but because the batteries-included philosophy genuinely saves time.&lt;/p&gt;

&lt;p&gt;Node.js earns its place in real-time, high-concurrency, and JavaScript-everywhere scenarios. When the project fits the runtime's strengths, it's the superior choice.&lt;/p&gt;

&lt;p&gt;The developers who get the most out of both are the ones who stop asking "which is better?" and start asking "which is better &lt;em&gt;for this&lt;/em&gt;?"&lt;/p&gt;

&lt;p&gt;That mindset shift — evaluating tools based on context rather than preference — is honestly one of the more useful things you can develop as a developer. The tool doesn't make the engineer. The judgment does.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built something with Laravel or Node.js? I'd genuinely like to hear about it. You can check out my &lt;a href="https://gagankumar.me" rel="noopener noreferrer"&gt;Laravel project&lt;/a&gt; and my &lt;a href="https://typeracer.gagankumar.me" rel="noopener noreferrer"&gt;Node.js project&lt;/a&gt; to see both in action.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://gagankumar.me/blog/laravel-vs-nodejs-which-backend-2026" rel="noopener noreferrer"&gt;gagankumar.me&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>node</category>
      <category>php</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How I Built a Real-Time Multiplayer Typing Game with React, TypeScript, Socket.IO &amp; MongoDB</title>
      <dc:creator>Gagan Kumar</dc:creator>
      <pubDate>Sun, 08 Mar 2026 07:30:00 +0000</pubDate>
      <link>https://forem.com/ggn_dev/how-i-built-a-real-time-multiplayer-typing-game-with-react-typescript-socketio-mongodb-2aoj</link>
      <guid>https://forem.com/ggn_dev/how-i-built-a-real-time-multiplayer-typing-game-with-react-typescript-socketio-mongodb-2aoj</guid>
      <description>&lt;p&gt;There's a specific kind of developer procrastination where you end up spending hours on a website that has nothing to do with what you were supposed to be working on. For me, that website was TypeRacer.&lt;/p&gt;

&lt;p&gt;I'd gone down a rabbit hole — racing strangers, watching my WPM climb, refreshing leaderboards at midnight — and somewhere between race 12 and race 13, the thought hit me: &lt;em&gt;I wonder how this actually works under the hood.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That curiosity turned into &lt;a href="https://typeracer.gagankumar.me" rel="noopener noreferrer"&gt;TypeRacrer&lt;/a&gt; — a full-stack competitive typing platform I built from scratch. And honestly, it ended up being one of the most technically interesting projects I've ever worked on. Not because it's the most complex thing I've built, but because it forced me to think about problems I'd never had to think about before.&lt;/p&gt;

&lt;p&gt;This isn't a code tutorial. It's the story behind the project — the decisions, the surprises, and the concepts that finally clicked while building it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Project Is Different From Most Portfolio Projects
&lt;/h2&gt;

&lt;p&gt;Most portfolio projects are essentially CRUD apps with a nice UI. You have a database, you fetch data, you display it, you maybe add some auth. That's completely fine — the real world runs on CRUD apps.&lt;/p&gt;

&lt;p&gt;But TypeRacrer introduced a fundamentally different challenge: &lt;strong&gt;state that is alive.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In a regular app, state lives in a database. It's static until someone changes it. You request it when you need it. In a real-time multiplayer game, state is constantly changing — and every player needs to see everyone else's changes &lt;em&gt;as they happen&lt;/em&gt;, not when they refresh the page.&lt;/p&gt;

&lt;p&gt;That one difference changes everything about how you architect your application.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Core Concept: WebSockets and Why They Matter
&lt;/h2&gt;

&lt;p&gt;To understand what makes this project tick, you first need to understand the difference between how a normal web app communicates versus how a real-time one does.&lt;/p&gt;

&lt;p&gt;In a typical web request, your browser asks the server a question and the server answers. That's it — the conversation is over. The server has no way to reach out to you first. It just waits to be asked.&lt;/p&gt;

&lt;p&gt;WebSockets flip this model entirely. Instead of a series of one-off conversations, a WebSocket creates a persistent, open connection between the client and the server. Either side can send a message at any time. The server can push data to the client without being asked.&lt;/p&gt;

&lt;p&gt;For a typing game, this is essential. When you type a character, your progress needs to appear on every other player's screen almost instantly. There's no "refresh to see updates" — the whole appeal of the game is watching the race happen live.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Socket.IO&lt;/strong&gt;, the library I used, sits on top of WebSockets and adds a clean event-based API on top of them, plus automatic fallbacks for older browsers and built-in room management. The concept of "rooms" in Socket.IO maps almost perfectly to race lobbies — a group of connected clients that receive the same events.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture Decision That Shaped Everything
&lt;/h2&gt;

&lt;p&gt;One of the first big decisions I had to make: &lt;strong&gt;where does the game state live?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My first instinct was the database. I already had MongoDB set up. Why not just save the race state there and have all clients poll it every second or two?&lt;/p&gt;

&lt;p&gt;I tried this mentally and immediately saw the problem. With multiple players typing simultaneously, you'd potentially have dozens of database writes and reads every second per race. And polling every second isn't even fast enough — a good typist averages 4-5 keystrokes per second. A one-second delay would make the game feel completely broken.&lt;/p&gt;

&lt;p&gt;The answer is &lt;strong&gt;in-memory state on the server&lt;/strong&gt;. Active race rooms live in the server's memory as plain data structures — fast to read, fast to write, no database overhead. When a player types, the server updates the in-memory state and immediately broadcasts the change to all connected clients.&lt;/p&gt;

&lt;p&gt;MongoDB only enters the picture when a race &lt;em&gt;finishes&lt;/em&gt;. The final results — who won, everyone's WPM, timestamps — get persisted to the database for leaderboards and history. But during the race itself, the database doesn't touch it.&lt;/p&gt;

&lt;p&gt;This taught me something important: &lt;strong&gt;not all data needs to be persisted, and not all data should be treated the same way.&lt;/strong&gt; The right storage mechanism depends entirely on how the data is used.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building the Monorepo: Sharing Code Between Frontend and Backend
&lt;/h2&gt;

&lt;p&gt;One of the most satisfying architectural decisions I made was setting this up as a &lt;strong&gt;TypeScript monorepo&lt;/strong&gt; using pnpm workspaces and Turborepo.&lt;/p&gt;

&lt;p&gt;What does that mean in practice? The project has three main parts: the React frontend, the Express backend, and a shared package that both of them import. That shared package contains the type definitions — the shapes of players, race rooms, and socket events — that both sides of the application agree on.&lt;/p&gt;

&lt;p&gt;Before I understood monorepos, I would have defined these types twice: once on the frontend, once on the backend, and inevitably let them drift apart. A small mismatch in a socket payload type could cause a bug that takes an hour to track down.&lt;/p&gt;

&lt;p&gt;With the monorepo setup, there's one source of truth. If I change what a "Player" object looks like, TypeScript immediately tells me everywhere in both the frontend and backend that needs to be updated. The compiler becomes a collaborator, not just a syntax checker.&lt;/p&gt;

&lt;p&gt;This is the kind of architectural thinking that scales. It's how large engineering teams maintain consistency across codebases that dozens of people touch.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Race Lifecycle: Thinking in States
&lt;/h2&gt;

&lt;p&gt;One of the most clarifying exercises in building TypeRacrer was mapping out the &lt;strong&gt;lifecycle of a race&lt;/strong&gt; as a state machine.&lt;/p&gt;

&lt;p&gt;A race room is never just "active" or "inactive." It moves through distinct phases:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Waiting&lt;/strong&gt; — the room exists, players are joining. We're holding until enough players are ready.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Countdown&lt;/strong&gt; — all players have hit ready. A 3-2-1 countdown begins. During this phase, inputs are locked. The tension before a race starts is actually a UX feature, not just a delay.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Racing&lt;/strong&gt; — the timer starts, inputs unlock, and progress is tracked and broadcast in real time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Finished&lt;/strong&gt; — all players have completed the text, or the timeout has triggered. Results are calculated, the leaderboard is shown, and results are written to MongoDB.&lt;/p&gt;

&lt;p&gt;Thinking in states like this prevented a whole class of bugs. Without it, you end up writing scattered if-checks everywhere: "should I accept input right now? is the race started? has it ended?" With a clearly defined state machine, the answer is always just: "what state are we in?"&lt;/p&gt;

&lt;p&gt;This mental model — breaking a complex flow into discrete, named states — is something I now apply to almost every feature I build.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Surprisingly Hard Problem: Measuring Typing Speed
&lt;/h2&gt;

&lt;p&gt;WPM (words per minute) sounds simple. Count the words, divide by time. Done.&lt;/p&gt;

&lt;p&gt;But in practice it's messier than that. What counts as a "word"? The standard in competitive typing is to treat every 5 characters as one word, regardless of actual word boundaries. This normalizes the score so that typing "a a a a a" doesn't give you an unfair advantage over typing "strength."&lt;/p&gt;

&lt;p&gt;What about errors? Raw WPM counts every keystroke regardless of accuracy. Adjusted WPM penalizes mistakes. For a competitive game, you want adjusted WPM — otherwise people just spam keys and don't care about accuracy.&lt;/p&gt;

&lt;p&gt;And then there's the smoothing problem. If you calculate WPM fresh every second, the number jumps around wildly — especially at the start of a race when the sample size is tiny. Early in a race, one slow second tanks your WPM. The solution is to calculate WPM based on the full elapsed time since race start, which naturally smooths out as more time passes.&lt;/p&gt;

&lt;p&gt;None of this is conceptually hard, but it requires careful thought about what you're actually measuring and why. It's a good reminder that even "simple" features have hidden depth when you think them through properly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real-Time Progress: The Optimization That Mattered
&lt;/h2&gt;

&lt;p&gt;When I first implemented progress broadcasting, I sent the entire room state to every client on every keystroke. The room object contains all players, all their stats, the race text, the room metadata — everything.&lt;/p&gt;

&lt;p&gt;In testing with a few players it seemed fine. But I realized quickly that this approach doesn't scale. With 4 players each typing 80 WPM, that's around 5-6 keystrokes per second per player. You're potentially broadcasting a large object 20+ times per second to every connected client.&lt;/p&gt;

&lt;p&gt;The fix was straightforward once I thought about it: &lt;strong&gt;only send what changed.&lt;/strong&gt; When a player's progress updates, broadcast just that player's updated data. The clients already have the full room state — they just need to know what one player's progress changed to. Smaller payloads, less bandwidth, less processing on every client.&lt;/p&gt;

&lt;p&gt;This principle — send the minimum necessary data — is one of the core optimization strategies in real-time systems. It's the difference between a snappy app and one that starts lagging when more people join.&lt;/p&gt;




&lt;h2&gt;
  
  
  Anti-Cheat: The Problem You Don't Think About Until You Have To
&lt;/h2&gt;

&lt;p&gt;Once I had a working leaderboard, I started thinking about something uncomfortable: what stops someone from just submitting a fake result of 500 WPM directly to the API?&lt;/p&gt;

&lt;p&gt;In single-player mode, the client sends the result to the server at the end of a race. If I trust that result blindly, the leaderboard is meaningless.&lt;/p&gt;

&lt;p&gt;The solution is &lt;strong&gt;server-side validation&lt;/strong&gt;. The server knows when the race started and what the text was. When a result comes in, it can sanity-check it: is this WPM physically possible given the time elapsed and the text length? If someone claims they typed 300 WPM on a 500-character passage in 10 seconds, the math doesn't add up and the result gets flagged.&lt;/p&gt;

&lt;p&gt;In multiplayer, this is even more natural — the server is already tracking every player's progress in real time, so it &lt;em&gt;knows&lt;/em&gt; the final state. The client doesn't submit results; the server calculates them. There's no vector for spoofed scores.&lt;/p&gt;

&lt;p&gt;Building this made me think differently about trust in web applications. The client is user-controlled. The server is yours. &lt;strong&gt;Never trust the client for anything that matters.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd Build Differently
&lt;/h2&gt;

&lt;p&gt;Every project teaches you things you wish you'd known at the start. TypeRacrer is no different.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Redis instead of in-memory Maps.&lt;/strong&gt; The current setup stores active race rooms in the server's memory. This means a server restart loses all active races, and it would be impossible to run multiple server instances (since they'd each have their own separate memory). Redis would solve both problems — it's fast like memory but persistent and shareable across instances.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A matchmaking queue.&lt;/strong&gt; Right now, players need to share a room ID to play together. A real matchmaking system that pairs players by skill level would make the game far more playable for strangers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;More robust reconnection handling.&lt;/strong&gt; If your connection drops mid-race, you're out. A proper reconnection system that restores your session would make the experience much less frustrating.&lt;/p&gt;

&lt;p&gt;These aren't oversights — they're trade-offs. Building the perfect version of everything would have meant never shipping at all. Getting a working version live and iterating is almost always the right call.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Bigger Picture: What Real-Time Development Teaches You
&lt;/h2&gt;

&lt;p&gt;I came into this project thinking it was about learning Socket.IO. I came out of it thinking differently about software architecture in general.&lt;/p&gt;

&lt;p&gt;Real-time applications demand that you think clearly about &lt;strong&gt;state&lt;/strong&gt; — who owns it, where it lives, how it flows, and how it gets synchronized. These questions exist in every application, but in a standard app you can get away with fuzzy answers. In a real-time multiplayer game, fuzzy answers become bugs that you can watch happen live in front of you.&lt;/p&gt;

&lt;p&gt;The monorepo taught me about &lt;strong&gt;shared contracts&lt;/strong&gt; — the value of having both sides of a system agree on the shape of data before either side is built.&lt;/p&gt;

&lt;p&gt;The anti-cheat work taught me about &lt;strong&gt;trust boundaries&lt;/strong&gt; — being explicit about what the server should verify and never delegating that responsibility to the client.&lt;/p&gt;

&lt;p&gt;The performance optimization taught me about &lt;strong&gt;minimal data transfer&lt;/strong&gt; — that what you &lt;em&gt;don't&lt;/em&gt; send is often as important as what you do.&lt;/p&gt;

&lt;p&gt;None of these are TypeRacrer-specific lessons. They show up everywhere in software engineering. But TypeRacrer made them concrete in a way that abstract reading never quite does.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://typeracer.gagankumar.me" rel="noopener noreferrer"&gt;TypeRacrer is live&lt;/a&gt; — open two tabs and race yourself, or share the link with a friend. The &lt;a href="https://github.com/Gagan1015/TypeRacer" rel="noopener noreferrer"&gt;source code is on GitHub&lt;/a&gt; if you want to dig into how any of this is actually implemented.&lt;/p&gt;

&lt;p&gt;If you're a developer thinking about building something similar, my honest advice: build it. Not to have a portfolio piece (though that's a bonus), but because the problems you'll run into will teach you things that are genuinely hard to learn any other way.&lt;/p&gt;

&lt;p&gt;Real-time applications are a different world. And once you understand how they work, you'll never look at the web the same way again.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://gagankumar.me/blog/real-time-typing-game-react-typescript-socketio-mongodb" rel="noopener noreferrer"&gt;gagankumar.me&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
