Published on

Ditch the server with Next.js and Prisma

Authors
  • avatar
    Name
    Thomas Quan
    Twitter
Reading time

Reading time

5 min read

Streamlining Full-Stack Development with Prisma and Next.js: A New Approach

In my journey of building my portfolio (read about it here), I recently discovered a powerful new way to integrate Prisma directly with Next.js, without the need for a separate server. This realization came as I was exploring Next.js's App Route feature through some example web pages showcased by the Next.js team. What I found has the potential to fundamentally change how we think about building full-stack applications, especially for prototypes and small projects.

A New Way to Build Full-Stack Applications

Traditionally, setting up a full-stack application required a dedicated server to handle API requests, data fetching, and complex logic. But with this direct integration of Prisma and Next.js, the process becomes much more streamlined. Developers can now build prototypes quickly, directly accessing their database within Next.js server-side components without needing to manage an entirely separate server instance.

This opens up several possibilities:

  • Rapid Prototyping: Streamlines the creation of proof-of-concept applications, making it easier to test ideas.
  • Direct Data Interaction: Reduces the layers between client and server, allowing for faster data retrieval.
  • Simplified API Management: The need to configure extensive APIs is minimized, as server-side logic can be written directly in Next.js API routes.

Reducing the Complexity of Data Fetching

One of the biggest advantages of this approach is the direct interaction between the client and the database, which reduces the complexity of traditional data-fetching methods. Instead of having multiple layers to manage data flow between the frontend and backend, developers can write their Prisma queries directly within Next.js.

For example, previously you might have had to create a getList method on the server, define the proper GraphQL types to mimic those queries, and then parse the data on the client side. With this new approach, it’s all handled in one place:

// Example: Direct data fetching with Prisma in Next.js API route
import { prisma } from '@/lib/prisma';

export default async function handler(req, res) {
  const blogs = await prisma.blog.findMany({
    where: {
      published: true,
    },
    orderBy: {
      createdAt: 'desc',
    },
  });
  res.json(blogs);
}

Complex Queries Made Simple

In traditional setups, dealing with complex queries—like using multiple where clauses or generating paginated lists—often meant creating detailed data mappings and mirroring server-side logic on the client. Here’s a comparison of how this process used to look versus how it is simplified now:

Traditional Method:

  1. Server: Write a getList method that exposes where clauses.
  2. GraphQL Schema: Define corresponding types to mirror the where clauses.
  3. Client: Consume and format the data correctly.

With Prisma + Next.js:

// Directly fetch paginated data with Prisma in a Next.js API route
const posts = await prisma.post.findMany({
  where: { category: 'Tech' },
  skip: (page - 1) * 10,
  take: 10,
});

This approach is far more intuitive, and the results can be directly consumed in your Next.js page components.

API Management is a Breeze

With Next.js handling the API routes, creating and managing RESTful endpoints is straightforward. Need a new endpoint? Just create a new file under /pages/api/ and write the logic using Prisma:

// Example API route in Next.js
export default async function handler(req, res) {
  const newPost = await prisma.post.create({
    data: {
      title: 'New Post',
      content: 'This is a new post created using Prisma and Next.js!',
    },
  });
  res.json(newPost);
}

There’s no need for a separate Express or NestJS server. Just define the logic, and you're ready to go.

The Downsides of This Approach

While the Prisma + Next.js direct integration is powerful, it comes with its own set of limitations:

  • Scalability Concerns: Without a dedicated server, it’s difficult to introduce complex business logic or domain-driven design principles like DDD, CQRS, or even microservices. As the project grows, managing everything within a single Next.js app can introduce technical debt.
  • Single Point of Failure: If the Next.js server instance goes down, your entire application—front-end and back-end—becomes inaccessible. This is something to consider if uptime and reliability are critical.
  • Limited Authentication Options: Advanced authentication workflows, like role-based access control (RBAC) or multi-factor authentication (MFA), can be more challenging to implement directly within Next.js:
// Simplified authentication example
export default async function handler(req, res) {
  if (!req.session.user) {
    return res.status(401).json({ error: 'Unauthorized' });
  }

  const posts = await prisma.post.findMany();
  res.json(posts);
}

For more complex scenarios, a separate authentication server may be needed, adding back some of the layers this approach initially sought to avoid.

  • Reusability and Modularity: This setup often doesn’t adhere well to the DRY (Don't Repeat Yourself) principle, as logic can become tightly coupled with the UI. For example, if the same logic is needed across multiple components, you might end up duplicating code rather than centralizing it in a service layer.
// Example of repeated logic across multiple API routes
const fetchPosts = async () => {
  return await prisma.post.findMany();
};

const fetchPublishedPosts = async () => {
  return await prisma.post.findMany({ where: { published: true } });
};
  • Not Ideal for Large Applications: For large-scale apps, the coupling between logic and UI can become a maintenance burden. It lacks the modularity you’d expect from a microservices architecture or a traditional backend with clear separation of concerns.

Final Thoughts

Using Prisma directly with Next.js offers a fresh and streamlined way to build small to medium-sized applications quickly. It simplifies the development process by reducing the need for a separate server and API layers, making it perfect for rapid prototyping. However, for larger projects or applications requiring more robust business logic, this approach may not be suitable. As always, it’s about choosing the right tool for the job and understanding when to embrace simplicity and when to scale up.