Do we really need a backend now? Can we build applications without backend with Next.js 14?

Do we really need a backend now? Can we build applications without backend with Next.js 14?

ยท

5 min read

In the rapidly evolving landscape of web development, the role of the backend has been a critical component for many years. However, with the advent of modern frameworks like Next.js 14, the question arises: "Do we really need a Backend now? Can we build applications without backend with Next.js 14?"

Introduction

If you know that I have been writing articles for 2 years or so and I have been in the software development world for more than 3 years and I enjoy writing and building backends more than frontends! Yeah, it's true, I enjoy writing backend more than frontend. But with the evolving Next.js Server Components and Server Actions. Do we still need a backend? Well, in my opinion, We don't.

Why we don't need a backend?

When I say that we don't need a backend, I mean that a lot of applications that we are building right now (SaaS, AI Tools, etc.) don't really need a separate backend. That said, I do think that a lot of large-scale applications still need a custom backend, Why?

Why do we need a backend?

So now let's think more and talk about why we need a backend. For me, it's simple that syncing a lot of data with the database and other external APIs just sucks, that's why I like to have a custom backend for any large-scale applications. Let's take it a step further by explaining to you with code examples.

How do you really build an application without a backend with Next.js 14?

So first, How do you really build an application without a backend with Next.js 14? Simply, you use a lot of third-party APIs to do so, like Clerk or Auth.js for Authentication, Stripe for payments, etc., you integrate a database into it using Prisma, and you use tRPC to build Type-safe APIs on the fly.

This seems like a good architecture but this comes with a lot of problems, one problem that is like you have to always solve is the external APIs you are using. You have to sync them with your database, like, when a user registers in your app and you are using Clerk, you then have to store the same user object in your database if you want to do something else with that user, it's also the case when you want to add some more properties to the user like you want to add a bio property on the user, you will still have to add the user in the database. This just doesn't stay limited to the user object in Clerk, but with other APIs like if you are using Sanity for content management, you still wanna sync the content with the database. To be honest, this sucks for any large-scale application.

Another problem is scalability this is the biggest problem because you're going to deploy your Next.js application on something like Vercel. Which we can say is amazing but not that much, like think about it, you are deploying a whole server to a platform that is built for deploying frontend.

But then there is one thing that I absolutely love when building these types of applications and that is the flow of data fetching, yeah, the flow is amazing. If you wanna fetch something from the database, no problem, here you go:

import { db } from "@/lib/db"

async function Page() {
    const posts = await db.posts.findMany({}) // it's type safe ๐Ÿš€

    return (
        <ul>
            {posts.map(post => (
                <li key={post.id}>{post.title}</li>
            )}
        </ul>
    )
}

export default Page

If you wanna create a tRPC query to query something from the server and then use it in client components, no problem, here you go:

// @/server/index.ts

import { publicProcedure, router } from "./trpc";
import { db } from "@/lib/database";
import { TRPCError } from "@trpc/server";

export const appRouter = router({
  getAllPosts: publicProcedure.query(async () => {
    const posts = await db.posts.findMany({
      orderBy: { createdAt: "desc" },
    });

    if (!posts) throw new TRPCError({ 
        code: "NOT_FOUND", ,
        message: "There are no posts"
    });

    return { posts };
  }),
});

export type AppRouter = typeof appRouter;

Now use this query to fetch this data on a client component:

"use client";

import Link from "next/link";
import { trpc } from "./_trpc/client";
import Loading from "@/components/Loading";

export default function Home() {
  const { data, isLoading } = trpc.getPosts.useQuery(); // it's also type-safe ๐Ÿš€

  if (isLoading) return <Loading />

  return (
    <main className="py-12 flex flex-col items-center">
        {posts.map(post => (
            <Link href={`/posts/${post.slug}`}>{post.title}</Link>
        ))}
    </main>
  );
}

I absolutely love this flow, for the simplest thing you don't have to create another endpoint on the backend and then fetch it on the client. You can either get it directly using Server Components or write a tRPC query to get it on a client component. Now let's talk about how you build an application with a backend and what I personally like.

How do you build an application with a backend?

Now here's where things get interesting for me, I absolutely love a few backend technologies specifically Nest.js, Express, Prisma, and obviously GraphQL. I have written a lot of articles related to these technologies, but, right now let's talk about some major stuff. The first thing that I like about building my own backend is, that it's mine, I have full control over it, and I can do anything I want in it, I am not limited to any point. I can build my own Authentication system, I can build my own API endpoints or queries/mutations. I can use anything I want. I am going to link some of my articles here so you can learn more about the backend:

and there are much more articles that I have written on backend technologies, you can check them out on this blog...

Did you find this article valuable?

Support Nouman Rahman by becoming a sponsor. Any amount is appreciated!

ย