Arno shares his best practices for designing robust Next.js full stack applications, drawing from experience with large-scale projects like Alibaba's DingTalk and personal endeavors. This guide covers key aspects of Next.js web application design.
Server-Side Architecture in Next.js
General API Design and Architectural Styles for Web Applications
-
Adaptive Architecture Design - Architecture should evolve throughout different stages of a project's lifecycle. Don't default to complex patterns like
micro-services
for every project, especially for smaller initiatives or MVPs. Instead, embrace an evolutionary approach where architecture adapts to changing requirements and scale for your web application design. -
Strategic API Design for Modern Web Architectures - Leverage RESTful principles and adhere to strict design guidelines. When appropriate, consider RPC frameworks like
GraphQL
orgRPC
. Carefully evaluate communication protocols before integrationβavoid unnecessary complexity unless specific requirements justify it. -
Prioritizing Stateless and Serverless Architecture in Next.js - Design services to be inherently scalable and manageable by prioritizing stateless architectures and serverless deployment models. Platforms like
Vercel
provide optimized serverless environments for Next.js applications. Alternative deployment options includeAliyun
orCloudFlare EdgeWorker
services. This is a key consideration for modern full stack app architecture. -
Robust API Management - Implement versioning (e.g.,
/api/v1/*
) to maintain backward compatibility for both public and internal APIs. Adhere to standardized specifications likeOpenAPI
orSwagger
to ensure comprehensive documentation and usability. As project complexity increases, select appropriate methodologies to manage this complexity based on your specific use cases. -
Leveraging Next.js for Full Stack Development - Utilize Next.js's comprehensive full-stack features to rapidly develop and validate concepts. Implement a layered architecture approach, introducing separation as complexity grows to maintain maintainability and scalability.
The following diagram illustrates a common layered architecture pattern for Next.js full stack applications:
+-----------------------------------------------------------------------------------------------+
| CLIENT |
+-----------------------------------------------------------------------------------------------+
| | |
| Page Request | API Request | Server Action
β β β
+-----------------------------------------------------------------------------------------------+
| CONTROLLER LAYER |
| |
| +-----------------+ +------------------+ +---------------------+ |
| | Page Route | | API Route | | Server Action | |
| +-----------------+ +------------------+ +---------------------+ |
| | | | |
| +-------------------------------------------------- |
| | |
+-----------------------------------------------------------------------------------------------+
| Call
β
+-----------------------------------------------------------------------------------------------+
| SERVICE LAYER |
| |
| +------------------+ +------------------+ +------------------+ |
| | Domain Service A | | Domain Service B | | Domain Service C | |
| +------------------+ +------------------+ +------------------+ |
| | | | |
| +-------------------------------------------------- |
| | |
+-----------------------------------------------------------------------------------------------+
| Use
β
+-----------------------------------------------------------------------------------------------+
| MANAGER LAYER |
| |
| +------------------+ +------------------+ +------------------+ |
| | Manager A | | Manager B | | Manager C | |
| +------------------+ +------------------+ +------------------+ |
| |
+-----------------------------------------------------------------------------------------------+
| Access
β
+-----------------------------------------------------------------------------------------------+
| DATA PERSISTENCE LAYER |
+-----------------------------------------------------------------------------------------------+
Next.js Application Layer (Controller / Routes)
Controller
/ Routes
: for next.js is api-routes use Function
programming style, it plays a role as traditional Application Layer in your Next.js project structure.
- handle the request and response in Web and Application layer
- use private folder
_folder
start with_
to handle the private routes and api routes, which is not exposed to the public and not be indexed by the next.js router - make full use of next.js' conventions of
default
,layout
,loading
,error
,not-found
,sitemap
,robots
etc to handle the common logic easily- upcoming
forbidden
,cache directive
convention
- upcoming
- group the routes by
(groupName)
to separate the routes by different business features or modules - deal with the error for web or other protocol response, handle error codes and error message -> see the following error handling patterns section
- return
unified
response or service result object to be handled by the client - controller can be write in global
middleware
function (similar tokoa
) or wrapped by high-order function to handle the common logic including:- composable
Page Route
- composable
API Route
-> can directly call from server-actions - composable
Server Action
- composable
// API Route
export const POST = composeAPIRoute([
authUser,
authBizLogic,
async (req, res, ctx) => {}
])
// Page Server
export const getServerSideProps = composePageServer(
function (params, ctx) {
return <div>...</div>
},
[authUserInPage, authBizLogicInPage]
)
// Server Action
export const serverAction = composeServerAction([
authUserAction,
authBizLogicAction,
async (req, res, ctx) => {}
])
Sample Code Here: https://github.com/SurfaceW/arno-packages/tree/main/server/next
- better with 0 business logic in this layer
Service Layer in Next.js Full Stack Architecture
Service
: module to handle the business logic with encapsulated domain service & managers in OO style, crucial for a clean Next.js full stack architecture.
- try to use Domain Driven Design principle to provide domain service to handle the business logic, persist data and other domain related tasks
- can not throw error and should deal with the error in the service layer
- return
unified
response or service result object to be handled by the controller - use Swagger or other formal API Specs to integrate with third-party services vendor, make sure the API is well documented in code and use tool to generate those API client code
- try Interface Oriented design for various kinds of similar services, e.g.
*.service.impl.ts
- obey the Rules of OO BP, such as SOLID, DRY, KISS, etc.
- use DI style to inject the dependency and build DI driven service dependency and instance management
example of this layer code can be:
export class ElaboratorService {
private prisma = prismaClient
constructor() {}
// handle error and unified response
@WithServiceResult()
async updateElaborator(data: Prisma.ElaboratorUpdateInput) {
return (await this.prisma.elaborator.update({
where: {
id: data.id as bigint
},
data: data
})) as any as ServiceResult<Elaborator>
}
}
Manager Layer for Reusable Business Logic in Next.js
Manager
: module to manage the business logic as the reusable code, in OO style, enhancing the maintainability of Next.js applications.
- can throw error and deal with the error in the service layer, or higher level
- can be used in the service layer or other manager layer, also in the controller layer (server actions, routes, etc), but not recommended in the higher layer
- optional DI(Dependency Injection) provided for service layer
- make it more atomic and modular to encapsulate the business logic
utils
andhelper
functions can be put here as a sub-category of manager
Data Persistence Strategies for Next.js Applications
- Precisely choose SQL or NoSQL DB type for your business features; selecting appropriate data persistence solutions is key for building scalable Next.js applications. Don't forget to add redundancy and backup for the data.
- pick ORM or other DB service vendor to handle the data persistence like
Prisma
orTypeORM
with migration and schema management - always start design your db considering the data scaling and data consistency, distributed db and sharding, etc, when you truly need it. e.g. your data-records is over 1M or 10M or 100M in 3 year, etc, they should plan to use different type of db technology and design the data model and data structure in the different ways.
- consider use
Serverless
DB to handle the data scaling and management, e.g.[PlanetScale](https://planetscale.com/)
- in the recent days, quick prototyping by using Supabase is a good choice, but do not use it in the production environment.
- consider use
- carefully design the data model and data structure in the DB, make full use of the DB index and query optimization, but do not over optimize the db query.
- follow the Community's SQL, table-creating, index-creating, query-optimization, etc, best practice guide. -> e.g. Alibaba's Java Developer Guide show some best practice guide for MySQL and DB.
- Make full use of DB design and optimization strategies, such as:
- use transaction and isolation to handle the data consistency and integrity for sensitive data and operations
- use sharding and partition to handle the data scaling and performance
- use replication and backup to handle the data redundancy and backup
- use index and query optimization to handle the data query and performance
- use schema and migration to handle the data schema and data structure
- use replication and backup to handle the data redundancy and backup
Next.js Framework: Best Practices
- use turbopack first for faster development
- git-submodules + npm packages to handle different level of code share and the monorepo and multi-repo, for AI Era, choose simple repo with shared code is better than npm dependency, which is more flexible and easy to maintain.
- make full use of Next.js' layered cache system in page / routes,
React.cache
, fetch cache, client cache, more with external redis-cache, etc.- β¨ Next.js Cache System read carefully and use it wisely π
- use EdgeRuntime for handling the static content and fetch related simple tasks, such as middleware, etc.
- use Rust built WASM for high-performance computing tasks, e.g. calculate tokens of large data via tiktoken
- separate the
ServerComponentData
/ClientComponentData
, use it to handle the server-side rendering and client-side rendering cleverly (key aspects of Next.js server components and client components).- ServerComponent are fit for fast loading and content first page, which is good for SEO and SNS sharing and also be very fast for user interaction
- ClientComponent are fit for dynamic and interactive page, which is good for user interaction, for app like web-app, mobile-app, etc.
- combine with Client and Server component together is progressive enhancement way ~
- use library internal impl of basic component:
- use CDN for static files and contents
- use
next/image
etc. for resource optimizations - use
next/script
for script optimizations - use
next/link
for link optimizations - use
next/font
for font optimizations - ... see the next.js official doc for more details
- make use add
metadata
ability inset next.js pages for SEO and SNS sharing- add
robots.txt
andsitemap.ts
for SEO and SNS sharing, including open graph, twitter card, etc. - integrate service to Google Analytics, Google Search Console, Google Tag Manager, etc. for SEO optimization
- add
- use
web-vitals
to monitor the performance of the Next.js app page rendering - boost performance with Next.js
dynamic/import
withSuspense
andReact.lazy
mechanism for async components loading on demand - use
server
/client
/shared
folder to separate the code for different runtime - Check every possible config in
next.config.js
for optimizing Next.js app performance and security before you go to production. - use
sever-action
to avoid duplicated API route declare and seamlessly handle the server-side logic and client-side logic together (a powerful feature of the Next.js framework). - next.js middleware is good for i18n, auth, analytics, redirect, CORS, etc. scenarios, but do not overuse it, use it wisely for targeted routes and limited scenarios
- next.js > 15 middleware can run both edge and node runtime, handle common request logic in
middleware
and usenext/server
to handle the request and response
- next.js > 15 middleware can run both edge and node runtime, handle common request logic in
Server Actions FIRST!
Other official recommendations guides you must know:
- auth
- analytics
- ci-building
- i18n
Make full use of libs, tools, services, platforms
Avoid reinventing the wheel, use the best tools and libs in the Node / JavaScript Ecology.
Supabase
in the recent years is a good example of using the best tools and libs in the Node / JavaScript Ecology to build a full-stack app.
- ai-sdk is great way to handle the AI related tasks (llm, reAct, agentic app creator, etc)
- DB layer can be presented by
Prisma
ORM or other db service vendor - use Jest or other testing service to handle the unit test and integration test
Playwright
is a good choice for end-to-end testing
- error / request trace with trace API & tools, such as
Sentry
/Raygun
- logs with log API & tools including SLS / ELK for logging and debugging
- consider use
GlobalRef
for long-time existed node-runtime instead of serverless runtime - add unit tests to cover basic lib / tools and key business logic
- use config middleware such as
EdgeConfigs
for app configs sync and sync to different server instances - use
Redis
or other memory shared to handle the cache and session shared by different server instances (vercel kv, redis, etc) - consider use the AB Test service or Gray Release service to handle the feature release
- consider use right choice of message queue middlewares to handle the async tasks and event-driven tasks, for larger traffic
DevOPS
- consider serverless first deployment and scaling, without the need of K8S or other container service to reduce the complexity and cost, unless you have a very complex and large scale application
- use Github Actions to handle the CI/CD pipeline and production process for small and medium scale application in light-weight and simple way
- for MVP and PMF, the Vercel is the best choice for the starter, for its really easy to use and deploy without extra considerations
Next.js official doc given the choices that support deploy Next.js in different platforms and scenarios.
Recommended Directory Structure for Next.js Projects
A well-organized directory structure is vital for maintaining large Next.js projects. Hereβs a recommended structure for your next.js full stack app:
.
βββ app
βββ components
βββ configs
βββ data
βββ tools
βββ i18n
βββ lib
βββ prisma
βββ public
βββ services
βββ tailwind.config.ts
βββ tsconfig.json
βββ .context -> AI Era, use .context to handle the context of the app, system instructions for prd, tech, product, project, etc.
βββ types
The dependency of the directory structure:
- base functionality
public
: the public static filesprisma
: the prisma db schema and model- ...
- global shared
tools
: global tools for the app (scripts are also included here)types
: global type use for external lib or low-level common shared typesconfigs
: global configs for the appi18n
: global i18n for the app
- share logic
lib
: the shared lib and tools, is the basic blocks shared for upper layers, such asutils
,helpers
,constants
,bizTypes
,manager
, etc.services
: the shared servicescomponents
: the shared components
- application logic
app
: the main app entry of next.js app
more detail:
- `app`: the main app entry of next.js app
- `[bizRoute]`: the business route
- `page.tsx`: the main page of the route
- other nextjs conventions named files
- `*.server.tsx`: biz server component
- `*.client.tsx`: biz client component
- `api`: the api routes
- `/v1`: the versioned api routes for public use
- `*.ts`: the api routes
- `components`: the shared components
- `server`: the server-side components
- `client`: the client-side components
- `shared`: the shared components
- `configs`: the app shared configs
- `lib`: the shared lib and tools
- `server`: the server-side lib and tools
- `client`: the client-side lib and tools
- `shared`: the shared lib and tools
- `*.manager.ts`: the biz manager that can be shared
- `services`: the shared services
- `server`: the server-side services
- `client`: the client-side services
- `shared`: the shared services
- `*.service.ts`: the biz service that can be shared
- `public`: the public static files
Or for unified business logic module, you can group file within a module folder for more clear and easy to maintain:
- `module-name`
- `*.client.ts(x)`
- `*.server.ts(x)`
- `*.shared.ts(x)`
Full example could be:
βββ elaborator
βΒ Β βββ client
βΒ Β βΒ Β βββ flow.manager.ts
βΒ Β βΒ Β βββ position.manager.tsx
βΒ Β βββ server
βΒ Β βΒ Β βββ elaborator.base.task.ts
βΒ Β βββ shared
βΒ Β βΒ Β βββ elaborator.diagram.manager.ts
βΒ Β βΒ Β βββ elaborator.enum.ts
βΒ Β βΒ Β βββ elaborator.type.ts
βΒ Β βββ tasks
βΒ Β βββ edoc-generator
βΒ Β βΒ Β βββ doc-generator.server.ts
βΒ Β βββ mental-framework-generator
βΒ Β βΒ Β βββ mental-framework-generator.server.ts
βΒ Β βββ problem-outline-generator
βΒ Β βββ problem-outline-generator.server.ts
βΒ Β βββ problem-outline.node.client.tsx
- control the naming length of your file, use
path
as the context, do not make file name too long
My Practicing:
- π Github - arno packages: my basic packages which work and follow the principles above.
Web Client Architecture with React and Next.js
React Best Practices for Next.js Frontend Development
Arno used to summary the React Best Practice Guide in the past ππ»
- Separate the Logic and View in different layers, a core principle for maintainable frontend architecture in Next.js.
- JSX / Component should be purely use Functional way
- DO NOT write chunks of business logic such as handler / callbacks / hooks in the JSX / Component use a function from a client manager or service
- try Server Actions if possible to handle the server-side logic and client-side logic together in a more simple and clear way
- separate the
State
of client in different layersServerState
: state managed by server- use hooks for location / url pathname / search-params / next-params, etc, as state of server ...
- use
SWR
orReactQuery
to handle the server state and cache or directly handle it by page in server component to reduce the complexity in client
SharedContext
usually do not mutate that often, useReact.Context
to handle the shared state and context, such as Theme, Locale, User, etc as Global shared context.SharedState
useredux
,zustand
orrecoil
with logger and middleware to handle the shared better immutable state among components which can be easily visualized, traced, logged and debugged.ReactiveState
consider to useRxJS
orMobx
to handle the reactive state and event-driven stateLocalComponentState
try to useuseState
anduseReducer
to handle the local state of the component
- Hook-Style First to encapsulate the logic and make it more reusable among components and reduce the code in JSX component body, which make the code more clear and easy to maintain
- Carefully Design the Data Flow from server to client, avoid duplicated data fetching with cache and state management, by clearly use client fetch and server fetch (RSC) together.
Logic Encapsulation
Encapsulating client-side logic is one of the Next.js frontend best practices for building modular and testable web applications.
- Service similar to the server service, client service and encapsulated apis to invoke by
useSwr
oruseQuery
or other API client.- some typical examples are:
- the db-service of local
- the local-data-cache service
- the Restful API service
- service can encapsulate the logic and handle the error and response elegantly, which should not emit exceptions or errors
- service can coordinate the different services or manager to handle the complex logic
- service can directly invoke server-action to handle the server-side logic and client-side logic together in a more simple and clear way
- ...
- some typical examples are:
- Manager similar to the server manager, client manager and encapsulated business logic to be invoked by the service or component
- some typical examples are:
- the business logic manager
- the logs manager
- the event-pub-sub manager
- ...
- manager are the basic building blocks of the business logic and can be reused by different services
- manager can throw error without extra handling process which shall delegate to the service to handle it properly
- some typical examples are:
Tips for Performance & Storage
- use web worker to handle the heavy computing tasks, running in a background thread
- use Rust / WASM to handle the heavy computing tasks
- use Service Worker (PWA) to handle the offline and cache data and requests
- use local-db such as
IndexedDB
to handle the local storage and cache, use encapsulated manager to handle the local-db operations - use
localStorage
/sessionStorage
to handle the local storage and cache for use preference and temporary data cache
Network and Communication
- use WebRTC / Websocket for more real-time and interactive tasks
- use native
fetch
API first other than ajax or other network communication mechanism - use HTTP stream with Fetch API to handle the large data transfer and stream data for AI Era streaming data, consider use ai-sdk's streaming data / UI RSC(React Server Component) to handle the streaming data and UI renderings
Tips for UI & UX
- use
tailwindCSS
for css framework, it is truly good for AI and with VSCode extension, it is lightweight and easy to use without add extra files and complexity to existed codebase. - use
shadcn/ui
(shadcn/ui.com) orAntD
(lobeUI) orMaterialUI
with highly customizable theme and complex logic and functionality encapsulated - use
Storybook
or other UI testing service to handle the UI testing and visual testing for UI components - consider compatibility in different viewport, size, versions and more
- follow the BP of Mobile Web App development principles, such as Responsive Design, Dark Scheme Compatibility, A11Y and I18N, Animation and Transition, etc.
- follow and apply some web dev guides of Google / Apple / Microsoft / Mozilla / W3C ...
- consider obey the A11Y and I18N principles
- add some reasonable animation and transition to make the UI more vivid and interactive
- Responsive Design FIRST
- Dark Scheme Compatibility
- use
SSG
/SSR
/ISR
/CSR
to handle the different rendering strategy to boost the performance and user experience, use hybrid approach to handle the complex and dynamic page rendering.SSG
: Static Site Generation, generate the static html files at build time, and serve the static html files at runtimeSSR
: Server Side Rendering, render the html on the server side, and send the html to the client sideISR
: Incremental Static Regeneration, regenerate the static html files at a certain intervalCSR
: Client Side Rendering, render the html on the client side
- ...
Domain Specified Technology
Do not create the wheel again, use the domain specified technology to handle the domain specified tasks.
- TextEditor / Code Editor / Code Formatter / Linter ...
- Chart / Graph / Data Visualization ...
- Map / Location / Geo ...
- Real-time Collaboration ...
- 3D Rendering / 2D Rendering / Animation ...
- Audio / Video / Media ...
- Web XR / VR / AR ...
- ...
just ask AI with your scenarios to find those best options to use in your project, and do not reinvent the wheel.
Native Client
can wait to provide the BP for the native client, such as iOS / Android / Windows / Mac / Linux / WebOS / Tizen / HarmonyOS / ... in actions.
Arno is still exploring π§
Common Design Patterns and Conventions in Next.js Development
This section outlines common design patterns and conventions crucial for effective Next.js full stack development, helping developers build consistent and maintainable web applications.
Error Handling Patterns
0 error handling is the best error handling, but we can not avoid the error in the real world, so we need to handle the error elegantly and properly.
- low-level API should throw less error and handle the error in the high-level API
- use one place to handle the error and log the error, do not handle the error in different places with duplicated code
- use Native
Error
object both inServer
andClient
, and do not extend it to add complexity, use unified error msg format like:
[2024-03-12 12:00:00] [ERROR level] [ServiceName] [ErrorTypeCode] [ErrorMsg]
[?ErrorStack] if necessary
- for server response in HTTP Protocol in common, the error should make full use of HTTP status, code, message to describe the error in the response, other RPC protocol should follow their own error handling format pattern
- use the
warn
/error
andfatal
to handle the different level of error, for level of error and fatal, we should uselog
service to report for stability and reliability - use
try
/catch
to handle the error in the service / manager layer, and try-catch phrase block scope should be as less as possible to avoid the performance and ambiguity of the code - use
ErrorBoundary
to handle the error in the component client layer for displayfallback
info and error info both from client and server- like
error-boundary.tsx
implementation
- like
File Types and Conventions
*.type.ts
: the typescript language type that can be shared*.constant.ts
: the constant that can be shared*.util.ts
: the utility functions that can be shared*.manager.ts
: the biz manager that can be shared*.manager.impl.ts
: the implementation of an abstract manager*.service.ts
: the biz service that can be shared*.service.impl.ts
the implementation of an abstract service*.store.ts
: the biz store of zustand or any other react-state manage units that can be shared*.hook.ts
: the biz hook that can be shared*.client.tsx
: the client-side component that can be shared*.server.tsx
: the server-side component that can be shared*.server.action.ts
: the Next.js' server action that can be shared among server and client*.shared.tsx
: the shared component that can be shared*.[designPatterName].*.ts
: the design pattern implementation in the codeaccount.adapter.impl.ts
the adapter pattern implementation of specific service
Class methods naming conventions
- use verb first and noun second for the method name, e.g.
createUser
,getUser
,updateUser
,deleteUser
- for
CRUD
operations of a service, usecreate
,get / list
,update
,delete
as the method name prefix - for common operations like
search
fetch
count
aggregate
... as the method name prefix - for common state-trans operations like
init
start
stop
pause
resume
... as the method name prefix - use
batch
bulk
multi
as the method name prefix for batch operations
Reference
My Related Articles:
notes on this article
- 2024-02-02: initial version
- 2024-03-12: bugfix and add more details for some topics, add
db
related topics guide - 2024-03-29: add directory naming conventions for components
- 2024-10-21: upgrade recent server-action bp and add more details for the recent experience accumulated
- 2025-05-13: keep updated with latest knowledge and version of next.js and other related technologies
- 2025-05-29: database related topics and best practices updated, add more practical experience in AI Era
crafting:
- Partial-rendering BP for performance optimization
- mdx now use mdx to write