Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 214 additions & 0 deletions src/routes/blog/post/using-nextjs-wrong/+page.markdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
---
layout: post
title: "You're probably using Next.js wrong"
description: Next.js isn't just React with extra steps. Learn when server-side rendering actually matters and how to use it properly with Appwrite.
date: 2026-01-15
cover: /images/blog/using-nextjs-wrong/cover.png
timeToRead: 8
author: atharva
category: tutorial
featured: false
---

Next.js has become the default choice for new React projects. But here's the problem: most developers treat it like React with a fancy router. They spin up a Next.js app, slap `'use client'` on every component, fetch data in `useEffect`, and wonder why when they face problems.

If that sounds familiar, you're shipping bloat for no reason.

# Next.js is not React

React is a UI library. It renders components and manages state. Routing, data fetching, and server logic are your problems.

Next.js is a full-stack framework that uses React for its UI layer. It gives you server-side rendering, static generation, API routes, and server components out of the box. These features exist for specific reasons, and if you're not using them, you're just adding complexity without benefit.

# Does SEO matter?

This is the deciding factor. If search engines need to index your content, you need server-side rendering. Client-rendered pages ship JavaScript that builds the DOM after load. Search crawlers can technically execute JavaScript, but they're inconsistent at it. Your e-commerce product pages, blog posts, and landing pages should render on the server or should be statically pre-rendered.

If you're building an internal dashboard or admin panel that lives behind a login, SEO is irrelevant. Client-side rendering is fine. You could use plain React with Vite and skip the Next.js overhead entirely.

# What server components actually solve

Server components aren't just about SEO. They solve three problems:

## Initial page load

Client components ship JavaScript to the browser, which then fetches data and renders. Users see a loading spinner. Server components fetch data and render HTML before anything reaches the browser. Users see content immediately.

## Bundle size

Every library you import in a client component ends up in your JavaScript bundle. Server components run on the server only. That heavy markdown parser or date library never touches the browser.

## Security

Server components can access databases and secrets directly. They are also capable of accessing environment variables without the `NEXT_PUBLIC_` prefix, since these components are run exclusively on the server.

# Converting a client component to a server component

Here's a typical client-side pattern:

```jsx
"use client";

import { useState, useEffect } from "react";
import { databases } from "@/lib/appwrite";

export default function Products() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
databases
.listRows({
databaseId: DATABASE_ID,
collectionId: COLLECTION_ID,
queries: [Query.equal("status", "active")],
})
.then((res) => setProducts(res.rows))
.finally(() => setLoading(false));
}, []);

if (loading) return <div>Loading...</div>;
return <ProductGrid products={products} />;
}
```

Here's the server component equivalent (without `'use client'`):

```jsx
import { createAdminClient } from "@/lib/appwrite/server";

export default async function Products() {
const { databases } = await createAdminClient();
const { rows } = await databases.listRows({
databaseId: DATABASE_ID,
collectionId: COLLECTION_ID,
queries: [Query.equal("status", "active")],
});

return <ProductGrid products={rows} />;
Comment on lines +53 to +88
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It uses the wrong TablesDB syntax

}
```
Comment on lines +77 to +90
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix inconsistent property name between factory and usage.

Line 81 destructures { databases } from createAdminClient(), but the factory definition at lines 102-118 returns { tablesDB, account }, not { databases }. This mismatch will cause a runtime error.

Additionally, line 85 uses Query.equal() without showing the import statement.

🐛 Proposed fix
-import { createAdminClient } from "@/lib/appwrite/server";
+import { createAdminClient } from "@/lib/appwrite/server";
+import { Query } from "node-appwrite";

 export default async function Products() {
-  const { databases } = await createAdminClient();
-  const { rows } = await databases.listRows({
+  const { tablesDB } = await createAdminClient();
+  const { rows } = await tablesDB.listRows({
     databaseId: DATABASE_ID,
     collectionId: COLLECTION_ID,
     queries: [Query.equal("status", "active")],
   });

   return <ProductGrid products={rows} />;
 }
🤖 Prompt for AI Agents
In `@src/routes/blog/post/using-nextjs-wrong/`+page.markdoc around lines 77 - 90,
The Products component destructures `{ databases }` from createAdminClient() but
the factory actually returns `{ tablesDB, account }`, causing a runtime error;
update Products to destructure `{ tablesDB }` (or change the factory to return
`databases`) and call `tablesDB.listRows(...)` instead of
`databases.listRows(...)`, and also add the missing `Query` import (e.g., import
{ Query } from "appwrite") so `Query.equal()` is defined; ensure you reference
the existing createAdminClient, Products, tablesDB/databases, and Query symbols
when making the change.


# Setting up Appwrite for server-side rendering

The Appwrite Web SDK runs in browsers. For server components, you need the Node SDK:

```bash
npm install node-appwrite
```

Create two client factories. The admin client uses an API key for public data:

```ts
// lib/appwrite/server.ts
import { Client, TablesDB, Account } from "node-appwrite";
import { cookies } from "next/headers";

export async function createAdminClient() {
const client = new Client()
.setEndpoint(process.env.APPWRITE_ENDPOINT)
.setProject(process.env.APPWRITE_PROJECT_ID)
.setKey(process.env.APPWRITE_API_KEY);

return {
tablesDB: new TablesDB(client),
account: new Account(client),
};
}
```
Comment on lines +102 to +118
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

fd "using-nextjs-wrong" --type f

Repository: appwrite/website

Length of output: 42


🌐 Web query:

node-appwrite package TablesDB class API documentation

💡 Result:

The Node Appwrite TablesDB API docs (Node.js SDK) are in the Appwrite reference: it documents the TablesDB class and methods (listTables, getTable, createTable, createRow(s), getRow, listRows, updateRow/upsertRow, deleteRows, indexes, transactions, etc.). See the server-side Node.js TablesDB reference. [1]

If you need the SDK package details or to confirm which node-appwrite version contains TablesDB (some older releases lack it), check the node-appwrite npm / package page. Upgrade to a recent node-appwrite release if you get missing-export errors. [2]

Sources:

  • Appwrite TablesDB Node.js reference. [1]
  • node-appwrite package (npm / package info). [2]

🏁 Script executed:

rg "using-nextjs-wrong" --type md

Repository: appwrite/website

Length of output: 42


🏁 Script executed:

rg "createAdminClient" --type md --type ts -B 2 -A 20

Repository: appwrite/website

Length of output: 42


🏁 Script executed:

fd "page.markdoc" src/routes/blog/post --type f | head -20

Repository: appwrite/website

Length of output: 1369


🏁 Script executed:

ls -la src/routes/blog/post/ 2>/dev/null | head -30

Repository: appwrite/website

Length of output: 2395


🏁 Script executed:

rg "TablesDB" --type md --type ts -B 2 -A 5

Repository: appwrite/website

Length of output: 2649


🏁 Script executed:

fd "next" src/routes/blog/post --type d

Repository: appwrite/website

Length of output: 512


🏁 Script executed:

rg "node-appwrite" src/routes/blog --type md | head -20

Repository: appwrite/website

Length of output: 42


🏁 Script executed:

rg "createAdminClient\|createSessionClient" src/routes/blog --type md

Repository: appwrite/website

Length of output: 42


🏁 Script executed:

cat -n "src/routes/blog/post/using-nextjs-wrong/+page.markdoc" | sed -n '90,130p'

Repository: appwrite/website

Length of output: 1409


🏁 Script executed:

wc -l "src/routes/blog/post/using-nextjs-wrong/+page.markdoc"

Repository: appwrite/website

Length of output: 117


🏁 Script executed:

cat -n "src/routes/blog/post/using-nextjs-wrong/+page.markdoc" | sed -n '102,180p'

Repository: appwrite/website

Length of output: 2623


Move cookies import to improve code clarity in tutorial.

The cookies import on line 105 is not used in the createAdminClient() function (lines 107–117) but is required in createSessionClient() (line 128). Moving it to the second code block improves readability for tutorial readers by avoiding the appearance of unused imports.

♻️ Proposed fix

First code block (lines 102–118):

 // lib/appwrite/server.ts
-import { Client, TablesDB, Account } from "node-appwrite";
-import { cookies } from "next/headers";
+import { Client, TablesDB, Account } from "node-appwrite";

 export async function createAdminClient() {

Second code block (lines 122–138):

+import { cookies } from "next/headers";
+
 export async function createSessionClient() {
🤖 Prompt for AI Agents
In `@src/routes/blog/post/using-nextjs-wrong/`+page.markdoc around lines 102 -
118, The import of cookies is currently placed alongside createAdminClient where
it isn't used; remove the unused import from the file top and add/import cookies
only where createSessionClient is defined/used so the createAdminClient function
(and its imports: Client, TablesDB, Account, createAdminClient) shows only the
dependencies it needs and createSessionClient has the cookies import available;
update the second code block to include cookies and delete it from the first to
improve clarity.


The session client uses the logged-in user's session for protected data:

```ts
export async function createSessionClient() {
const client = new Client()
.setEndpoint(process.env.APPWRITE_ENDPOINT)
.setProject(process.env.APPWRITE_PROJECT_ID);

const session = (await cookies()).get("session");
if (session) {
client.setSession(session.value);
}

return {
tablesDB: new TablesDB(client),
account: new Account(client),
};
}
```

The difference matters. Admin client queries return all documents matching your query. Session client queries return only documents the user has permission to access.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rows, not documents

@atharvadeosthale this needs to be vetted


# Handling authentication

Login with a server action:

```tsx
// app/login/page.tsx
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { createAdminClient } from "@/lib/appwrite/server";

export default function LoginPage() {
async function login(formData: FormData) {
"use server";
const { account } = await createAdminClient();
const session = await account.createEmailPasswordSession(
formData.get("email") as string,
formData.get("password") as string
);

(await cookies()).set("session", session.secret, {
httpOnly: true,
secure: true,
sameSite: "strict",
expires: new Date(session.expire),
});

redirect("/");
}

return (
<form action={login}>
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit">Log in</button>
</form>
);
}
```

Protect routes with a layout:

```tsx
// app/(protected)/layout.tsx
import { redirect } from "next/navigation";
import { createSessionClient } from "@/lib/appwrite/server";

export default async function ProtectedLayout({ children }) {
try {
const { account } = await createSessionClient();
await account.get();
} catch {
redirect("/login");
}

return children;
}
```

# When to skip all of this

Use plain React when:

- Your app lives behind authentication
- Search engines don't need to index it
- You prefer client-side data fetching patterns
- You're building a very simple app that doesn't need server-capabilities

There's nothing wrong with client-side rendering. The mistake is using Next.js and not leveraging what makes it useful.

# Resources

- [Appwrite SSR documentation](/docs/products/auth/server-side-rendering)
- [Next.js App Router documentation](https://nextjs.org/docs/app)