Arno write his BestPractice while design next.js powered Apps in Alibaba's DingTalk and other larger projects.
Server
General API & Architecture Style
- architecture design should be varied, evolved with different stages of life cycle of the project, do not always use the same architecture style such as
micro-services
first, they are not always the best choice for starter or small project. - unleash the power of Restful API Design keep and follow strict API design principle, or other formal API design principle or RPC framework such as
GraphQL
orgRPC
, carefully choose to apply different communication protocols in one project, do not add complexity until it is really necessary. - Stateless and Serverless design first for service to be easily scaled and managed.
Vercel
provides a good serverless platform for next.js app. We can also deploy the serverless function inAliyun
orCloudFlare EdgeWorker
as service vendor. - for public API or internal API management, we should add version control in pathname such as
/api/v1/*
to make sure the backward compatibility, follow a standard API Design spec such asOpenAPI
orSwagger
to make sure the API is well documented and easy to use. API is complicated by the time the complexity of the project grows, there are many methodology to control such complexity, we need to pick a suitable one for the project. - Make full use of Next.js full-stack development ability to quickly build web app and validate your ideas try and fail fast. Add layers and separate apps into different layers when the project grows and the complexity increases.
Application Layer (Controller)
Controller
/ Routes
: for next.js is api-routes use Function
programming style, traditional Application Layer.
-
handle the request and response in Web and Application layer
-
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
- 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
Service
: module to handle the business logic with encapsulated domain service & managers in OO style
- Domain Driven Design provide domain service to handle the business logic
- can not throw error and should deal with the error
- 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
Manager Layer
Manager
: module to manage the business logic as the reusable code, in OO style
- can throw error
- optional DI 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 Layer
- precisely choose SQL or NoSQL DB type for your business features, 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
. - 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
- 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.
- use transaction and isolation to handle the data consistency and integrity for sensitive data and operations
Next.js Best Practice Guide
- use turbo-pack + git-submodules + npm packages to handle different level of code share and the monorepo and multi-repo
- make full use of Next.js' layered cache system in page / routes,
React.cache
, fetch cache, client cache, more with external redis-cache, etc. - try to use EdgeRuntime for handling the static content and fetch related simple tasks
- use Rust + 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
- use CDN for static files and contents, use
next/image
etc. for resource optimizations - use
next/script
for script optimizations - make use add
metadata
ability inset next.js pages for SEO and SNS sharing - boost performance with Next.js
dynamic/import
withSuspense
andReact.lazy
mechanism - use
server
/client
/shared
folder to separate the code for different runtime - check every possible config in
next.config.js
to optimize the 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
Make full use of tools / libs of Node / JavaScript Ecology
Avoid reinventing the wheel, use the best tools and libs in the Node / JavaScript Ecology.
- DB layer can be presented by
Prisma
ORM or other db service vendor - 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
Diamond
(Alibaba's config middleware) or Vercel Configs for app configs - use
Redis
or other memory shared to handle the cache and session shared by different server instances - consider use the AB Test service or Gray Release service to handle the feature release
- use
Kafka
or other message queue to handle the async tasks and event-driven tasks - use
K8S
or other container service to handle the deployment and scaling - use
Prometheus
or other monitoring service to handle the monitoring and alerting - use
Grafana
or other dashboard service to handle the dashboard and visualization - use
Jenkins
or other CI/CD service (Github Actions or Vercel) to handle the CI/CD pipeline and production process - use
Jest
or other testing service to handle the unit test and integration test - ...
lib and directory structure guide
Recommend directory structure for the next.js full stack app:
.
├── app
├── components
├── configs
├── data
├── tools
├── i18n
├── lib
├── prisma
├── public
├── services
├── tailwind.config.ts
├── tsconfig.json
└── 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
React Best Practice Guide
- separate the Logic and View in different layers
- 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
Logic Encapsulation
- Service liked 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
- ...
- some typical examples are:
- Manager liked 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
- use Rust / WASM to handle the heavy computing tasks
- use Service Worker to handle the offline and cache data and requests
- use local-db such as
IndexedDB
to handle the local storage and cache - 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 WebStream with Fetch API to handle the large data transfer and stream data
Tips for UI & UX
- use
TailwindCSS
orChakraUI
to handle the UI and layout - use
AntD
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 - consider compatibility in different viewport, size, versions and more
- follow the BP of Mobile Web App development principles
- 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
/Reactive
/Hybrid
to handle the different rendering strategy to boost the performance and user experience - ...
Domain Specified Technology
Do not create the wheel again, use the domain specified technology to handle the domain specified tasks.
- TextEditor / IDE / 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 ...
- ...
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 Patterns and Conventions
Provide some common patterns and conventions for the next.js full stack app development that developers can follow and use.
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-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