← all conversations

App Router Feedback

2025-07-0720 turns12,494 charsgpt-4o
nextjsapp-routercode-review

Summary

User built an invoice app with Next.js 14 App Router and Server Actions and asked for feedback on their implementation.

Messages

Built an Invoice App with App Router, Server Actions & Custom JWT Auth – Looking for Feedback Help Hey everyone 👋, I’ve been experimenting with Next.js 14 App Router and built a simple invoice management app. No next-auth, no external data-fetching libs, no client mutations — just native <form action={serverFunc}>, Prisma, and raw cookie handling. 🔧 Stack Overview: Frontend: Next.js App Router, TailwindCSS, React Hook Form + Zod Backend: Prisma + PostgreSQL Auth: Custom JWT auth with secure httpOnly cookies Data: Server actions only, no client fetching — all mutations handled through <form action={...}> 💭 Feedback I'm looking for: App Router usage: Am I using server actions efficiently, or abusing them? Could progressive enhancement be better leveraged? Custom Auth: Anything dangerous or insecure in my cookie + JWT logic? Prisma Decimal: Manually .toNumber()’ing Decimal before sending to client. Best practice? Code organization: I separated actions/, components/, lib/, etc. Is that sustainable long term? 🔗 Project: GitHub: github.com/greedypanda0/Billboo Live: bilboo.vercel.app Test User: email: kristy.bins@gmail.com password: password123 If you're building something similar, I’d love to hear how you’re handling session management, data sanitization, or performance tradeoffs in App Router. Thanks in advance 👊 Upvote 3 Downvote 4 Go to comments Share Share u/HP_US avatar HP_US • Promoted Say goodbye to boring busywork. The HP AI PC powered by the Intel® Core™ Ultra Processor automates repetitive tasks, so you can create, solve, and build. With the right tools, work doesn’t have to feel like work. Learn More hp.com Thumbnail image: Say goodbye to boring busywork. The HP AI PC powered by the Intel® Core™ Ultra Processor automates repetitive tasks, so you can create, solve, and build. With the right tools, work doesn’t have to feel like work. Join the conversation Sort by: Best Search Comments Expand comment search Comments Section u/fantastiskelars avatar fantastiskelars • 1h ago Profile Badge for the Achievement Top 1% Commenter Top 1% Commenter Is that the password to your email? Upvote 1 Downvote Reply reply Award Share Share Dreamer_Luck OP • 1h ago its a random account made by seeding Upvote 1 Downvote Reply reply Award Share Share u/No_Set7679 avatar No_Set7679 • 1h ago did you used refreshToken ?? Upvote 1 Downvote Reply reply Award Share Share Dreamer_Luck OP • 1h ago nope, the challenge did not say anything about refresh token or tight auth Upvote 1 Downvote Reply reply Award Share Share
⚙ model reasoning step
gpt-4o
⚙ model reasoning step
gpt-4o
⚙ model reasoning step
gpt-4o
⚙ model reasoning step
gpt-4o
⚙ model reasoning step
gpt-4o
⚙ model reasoning step
gpt-4o
⚙ model reasoning step
gpt-4o
⚙ model reasoning step
gpt-4o
⚙ model reasoning step
gpt-4o
⚙ model reasoning step
gpt-4o
⚙ model reasoning step
gpt-4o
⚙ model reasoning step
gpt-4o
⚙ model reasoning step
gpt-4o
⚙ model reasoning step
gpt-4o
Hey 👋 — great job shipping a fully-server-driven CRUD app in four days! Below is a “red-team” style walkthrough of the areas you asked about, plus a few optional hardening ideas. --- ## 1 · App Router & Server Actions | ✅ What’s solid | 🔍 What I’d tweak | |-----------------|------------------| |*All* mutations happen through `<form action={serverFn}>`, so you get free progressive-enhancement and zero JS fallback citeturn15search1 |Put each action as close as possible to the UI that calls it (co-locate). That keeps closure-based state local and makes revalidation (`revalidatePath` / `revalidateTag`) simpler. | |You avoided an extra network hop (no `/api/*` route). Good for latency.|If any component ever needs optimistic UI, wrap the action in a thin client wrapper and use `useTransition` + `pending`. That keeps the core action pure.| |Using `useFormStatus` / `useFormState` with React-Hook-Form is 💯.|Right now every submission re-renders the full page. For hot-paths (e.g. “mark invoice paid”) consider returning a small fragment and using `revalidateTag('invoice')` to surgically refresh only the list. | **Edge Functions vs. Node** If you stay on Vercel, your current setup runs in the Edge runtime only when the importing component is RSC-only. Because Prisma requires Node, that means the invoices list is already running in a Node lambda. Be explicit: add `export const runtime = "nodejs"` at the top of your server components so you don’t get an accidental edge-/node split later. --- ## 2 · Custom JWT Auth | What you’re doing | Risk / Fix | |-------------------|-----------| |JWT in a **secure, httpOnly, SameSite=strict** cookie 👌|Missing **CSRF** mitigation on state-changing POSTs. Either add a double-submit token or set `SameSite='lax'` *and* require a custom header on XHR/fetch forms.| |Short-lived access token (e.g. 15 min) *without* refresh token.|UX hit (users log in again after idle) and replay-window risk (stolen token valid for full TTL). Consider sliding sessions: <br>• Access token 5–10 min<br>• httpOnly refresh token 30–90 days<br>• Rotate both on every refresh.| |Password hashed with **bcryptjs**.|Cost factor 10 is fine for small projects. Set it via `bcrypt.hash(password, 12)` for production; that’s ≈ 100 ms on a typical 2025 laptop.| |Manual cookie parsing inside every action.|Move the verification into **middleware.ts** so every request has `cookies().get('access')`. Much DRYer.| Good reference: an httpOnly-cookie flow + refresh rotation pattern citeturn16search3 --- ## 3 · Prisma `Decimal` `toNumber()` works but silently rounds big values. Three alternatives: 1. **Use `transformer`** ```ts @@map("invoice") model Invoice { amount Decimal @db.Decimal(12, 2) @type("number") } ``` (Prisma 6+ supports experimental transformers behind a feature flag.) 2. **Return strings to the client** and format in the component: `Intl.NumberFormat(undefined,{style:'currency',currency:'USD'}).format(+amount)`. 3. **Write a helper** that walks the result tree and converts every `instanceof Prisma.Decimal` to `amount.toFixed(2)`. Community discussion here citeturn17search0. Pick #2 or #3 if you don’t want the experimental flag. --- ## 4 · Folder Structure The classic `app/`, `components/`, `lib/`, `actions/` split scales until you hit domain complexity. Two forward-compatible tweaks: * **Feature-first**: group by domain (`app/(invoices)/`, `app/(clients)/`). Co-locate forms, server actions, and UI there. * **`db/` or `services/` layer**: isolate *queries* from *commands*. Your server action then calls `invoiceService.create(data)`; swapping Prisma for Drizzle later becomes trivial. --- ## 5 · Other Quick Wins 1. **Schema validation** – You already use Zod on the client. Run the *same* Zod schema inside the server action before touching Prisma (defense in depth). 2. **Rate limiting** – Add a simple rate limiter to login & password reset endpoints (e.g. upstash/ratelimit). 3. **Observability** – Vercel’s `otel/insights` + `instrumentation.ts` gives you traces for each server action with two lines of config. --- ### TL;DR Roadmap 1. Add CSRF token or SameSite=lax+custom header. 2. Introduce refresh-token rotation middleware. 3. Switch from `Decimal.toNumber()` to string+format or the experimental transformer. 4. Gradually migrate to feature-based folders with co-located actions. 5. Sprinkle `revalidateTag` for snappier UX and keep actions colocated. Your stack choice is solid and *exactly* the direction the Next.js core team is pushing (forms → actions → db). Tighten auth, tidy decimals, and you’re production-ready. 🚀 Happy hacking!
gpt-4o
write in paragraphs
Absolutely — here’s a rewritten version of the feedback in clean, structured paragraphs: --- You’ve done a fantastic job embracing the Next.js 14 App Router and leaning into its server-first paradigm. Your use of `<form action={serverAction}>` is exactly in line with the direction the framework is headed. It enables progressive enhancement out of the box and keeps things simple by avoiding client-side data fetching or API route indirection. That said, you might consider co-locating server actions closer to the components that invoke them. This reduces cognitive overhead and makes revalidation (`revalidatePath` or `revalidateTag`) easier to manage. Additionally, for actions that might benefit from faster UI updates—like toggling invoice status—you could introduce a small client wrapper using `useTransition`, allowing for optimistic UI while keeping your business logic server-side. Your custom JWT authentication setup using secure, httpOnly cookies is also a solid start. However, there are a couple of areas where security could be improved. Since you're handling state-changing POST requests without a CSRF token, adding a double-submit CSRF strategy would be wise. Alternatively, you could relax your `SameSite` attribute to `'lax'` and require a custom header on any client-side fetches. This mitigates common CSRF attack vectors. Additionally, you currently don’t use refresh tokens, meaning short-lived JWTs will eventually expire and log users out. Introducing a refresh token pattern—where the access token lives for ~10 minutes and the refresh token for ~30–90 days, both rotated frequently—would improve UX and security without much complexity. Handling Prisma’s `Decimal` objects manually with `.toNumber()` can work short-term, but it carries rounding risks for larger or high-precision values. If you want a more robust solution, you could consider returning the value as a string and formatting it in your components using `Intl.NumberFormat`. Alternatively, Prisma now offers an experimental `transformer` API that can automatically serialize `Decimal` to `number`, but you’ll need to enable a feature flag and accept some risk with bleeding-edge tooling. For most use cases, string serialization followed by client-side formatting is the safest and most maintainable option. Your file structure—splitting `actions/`, `components/`, and `lib/`—is completely reasonable for a small-to-mid scale app. However, if the project grows, consider transitioning toward a feature-first folder structure. This means grouping related functionality (e.g. invoices, clients, auth) into their own folders within `app/`, with co-located components, actions, and styles. It scales better and matches how real-world products tend to evolve. Adding a lightweight `services/` or `db/` layer can also help decouple business logic from Prisma itself, making future refactors easier if you ever switch to Drizzle or another ORM. Lastly, consider layering on some production-friendly enhancements. Adding schema validation using Zod on the server side (even if it duplicates client checks) adds defense-in-depth. Rate limiting your login endpoint (e.g. via Upstash) is also smart. And if you want observability, consider enabling OpenTelemetry support via `instrumentation.ts` on Vercel—it’ll give you real-time traces for all server actions with minimal setup. --- Overall, you’re on the right path. You’ve avoided unnecessary complexity, embraced progressive enhancement, and handled authentication in a way that shows you understand the underlying web primitives. A few hardening improvements around auth and decimals, plus some architectural tweaks for scale, and your app will be rock solid. Great work!
gpt-4o
rewrite like a junior wrote it and shorter
Sure! Here's a shorter version written in a more junior, casual tone: --- Hey! I checked out your project and you’re doing a great job using the new Next.js App Router stuff. Using `<form action={...}>` for all your mutations is super clean and keeps everything server-side, which is what they recommend now. One tip though: maybe try putting your server actions closer to the components that use them—it makes things easier to manage, especially with stuff like `revalidatePath`. Your custom JWT auth with httpOnly cookies is solid, but there are a couple of things to watch out for. You’re not using CSRF protection, which can be risky for POST requests. Either add a CSRF token or use `SameSite='lax'` and check for a custom header. Also, adding a refresh token would make the login session last longer and feel more polished. For the Prisma `Decimal` stuff, using `.toNumber()` works but can mess up precision sometimes. A better way might be to send the value as a string and format it on the frontend. Your file structure looks good for now, but if the app gets bigger, it might help to group stuff by feature (like having all invoice files together). And if you want to make the app more production-ready, adding server-side Zod validation, rate limiting login, and some logs or metrics would be smart. Overall, it’s a great start and you’re doing things the modern way. Just a few tweaks and it’s solid. Nice work!
gpt-4o