Links#
https://nodejs.org/en/learn
https://nodejs.org/api/
https://nodejs.org/api/esm.html
https://nodejs.org/api/test.html
https://nodejs.org/api/process.html
https://docs.npmjs.com/cli/v11/configuring-npm/package-json
https://eslint.org/docs/latest/use/getting-started
1. Important Points#
Node.js 是 JavaScript runtime:
built on V8
event loop / non-blocking IO
good for API / worker / CLI / realtime apps
ecosystem uses npm packages
适合:
HTTP API
BFF / gateway
queue worker
CLI tool
realtime websocket service
serverless function
不适合:
CPU-heavy compute without worker thread / native addon
blocking sync IO in hot path
unbounded memory workload
核心原则:
use Node.js LTS
use ESM for new projects
one package manager per project
use npm ci in CI
validate external input
reuse clients and connection pools
log structured JSON in production
mental model:
JavaScript code
-> Node.js runtime
-> event loop handles async IO
-> libuv handles many OS async operations
-> npm manages packages
2. Environment Setup#
versions#
recommended:
Node.js LTS
npm bundled with Node.js
check:
node -v
npm -v
create project#
mkdir order-api
cd order-api
npm init -y
npm install express dotenv zod pino
npm install -D eslint @eslint/js globals vitest
dependencies:
express:
HTTP server framework
dotenv:
local .env loading
zod:
runtime validation
pino:
structured JSON logger
3. Project Structure#
order-api
├── package.json
├── eslint.config.mjs
├── .env.example
├── .env.local
├── src
│ ├── main.js
│ ├── app.js
│ ├── config
│ │ └── config.js
│ ├── logger
│ │ └── logger.js
│ ├── errors
│ │ └── AppError.js
│ ├── orders
│ │ ├── OrderRepository.js
│ │ ├── OrderService.js
│ │ └── OrderRoutes.js
│ └── shared
│ └── result.js
└── test
└── OrderService.test.js
structure rules:
main.js:
process entrypoint
app.js:
express app composition
config/:
environment config loading and validation
logger/:
logger singleton / factory
errors/:
application errors
feature folder:
repository
service
routes/controller
4. Package / Dependency Management#
package.json#
{
"name": "order-api",
"version": "1.0.0",
"type": "module",
"private": true,
"scripts": {
"dev": "node --watch src/main.js",
"start": "node src/main.js",
"test": "node --test",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"check": "npm run lint && npm test"
},
"dependencies": {
"dotenv": "latest",
"express": "latest",
"pino": "latest",
"zod": "latest"
},
"devDependencies": {
"@eslint/js": "latest",
"eslint": "latest",
"globals": "latest",
"vitest": "latest"
}
}
important:
type=module:
use ESM import/export
private=true:
prevents accidental npm publish
package-lock.json:
commit it
use npm ci in CI
import / export#
// createOrderId.js
import { randomUUID } from "node:crypto";
export function createOrderId() {
return `o-${randomUUID()}`;
}
import { createOrderId } from "./createOrderId.js";
ESM note:
local import path should include .js extension
5. Syntax You Need Most#
variables#
const orderId = "o-1001";
let status = "PENDING";
status = "PAID";
rule:
use const by default
use let only when reassignment is needed
avoid var
object / array#
const order = {
orderId: "o-1001",
userId: "u-1001",
amount: 99.9,
status: "PENDING"
};
const items = [
{ sku: "sku-1001", quantity: 1 },
{ sku: "sku-1002", quantity: 2 }
];
destructuring / spread#
const { orderId, userId } = order;
const paidOrder = {
...order,
status: "PAID",
paidAt: new Date().toISOString()
};
function#
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.quantity * item.price, 0);
}
const isPaid = (order) => order.status === "PAID";
async / await#
async function findOrder(orderId) {
return orderRepository.findById(orderId);
}
class#
export class OrderService {
constructor(repository) {
this.repository = repository;
}
async markPaid(orderId) {
const order = await this.repository.findById(orderId);
if (order === null) {
throw new Error("order not found");
}
return this.repository.save({
...order,
status: "PAID"
});
}
}
module builtin#
import { readFile } from "node:fs/promises";
import { createServer } from "node:http";
import { randomUUID } from "node:crypto";
rule:
use node: prefix for Node.js built-in modules
6. Error Handling#
app error#
export class AppError extends Error {
constructor(message, code, statusCode) {
super(message);
this.name = "AppError";
this.code = code;
this.statusCode = statusCode;
}
}
async error#
export class OrderService {
constructor(repository) {
this.repository = repository;
}
async markPaid(orderId) {
const order = await this.repository.findById(orderId);
if (order === null) {
throw new AppError("order not found", "ORDER_NOT_FOUND", 404);
}
if (order.status !== "PENDING") {
throw new AppError("order status is not pending", "INVALID_ORDER_STATUS", 409);
}
return this.repository.save({
...order,
status: "PAID",
paidAt: new Date().toISOString()
});
}
}
rules:
expected business error -> AppError
unexpected error -> log stack and return generic 500
never expose raw stack to client
7. Testing#
node:test#
import test from "node:test";
import assert from "node:assert/strict";
test("marks pending order as paid", async () => {
const repository = new InMemoryOrderRepository([
{
orderId: "o-1001",
userId: "u-1001",
amount: 99.9,
status: "PENDING"
}
]);
const service = new OrderService(repository);
const order = await service.markPaid("o-1001");
assert.equal(order.status, "PAID");
});
what to test:
service business logic
validation
repository
HTTP routes
error mapping
8. Lint#
install#
npm install -D eslint @eslint/js
eslint.config.mjs#
import eslint from "@eslint/js";
import globals from "globals";
export default [
{
ignores: [
"node_modules/**",
"coverage/**",
"dist/**"
]
},
eslint.configs.recommended,
{
files: ["src/**/*.js", "test/**/*.js"],
languageOptions: {
ecmaVersion: "latest",
sourceType: "module",
globals: {
...globals.node
}
},
rules: {
"no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}
],
"no-console": "off"
}
}
];
run#
npm run lint
npm run lint:fix
lint vs test:
lint:
catches code quality and common mistakes
test:
checks behavior
9. Configuration By Environment#
files#
config organization:
.env.example
committed template
.env.local
local development
not committed
.env.test
test defaults
staging / prod:
injected by deployment platform
.env.example#
NODE_ENV=local
PORT=3000
LOG_LEVEL=debug
DATABASE_URL=postgres://user:password@localhost:5432/order
PAYMENT_API_URL=http://localhost:8080
typed-like validation with zod#
import "dotenv/config";
import { z } from "zod";
const configSchema = z.object({
NODE_ENV: z.enum(["local", "dev", "test", "staging", "prod"]).default("local"),
PORT: z.coerce.number().int().positive().default(3000),
LOG_LEVEL: z.enum(["trace", "debug", "info", "warn", "error"]).default("info"),
DATABASE_URL: z.string().url(),
PAYMENT_API_URL: z.string().url()
});
const parsed = configSchema.safeParse(process.env);
if (!parsed.success) {
console.error(parsed.error.flatten().fieldErrors);
process.exit(1);
}
export const config = {
env: parsed.data.NODE_ENV,
port: parsed.data.PORT,
logLevel: parsed.data.LOG_LEVEL,
databaseUrl: parsed.data.DATABASE_URL,
paymentApiUrl: parsed.data.PAYMENT_API_URL
};
10. Logging#
import pino from "pino";
import { config } from "../config/config.js";
export const logger = pino({
level: config.logLevel,
base: {
service: "order-api",
env: config.env
}
});
rules:
JSON logs in production
pretty logs only local
include request_id / trace_id
redact secrets
11. Build / Run / Debug#
dev#
test#
lint#
production#
debug#
node --inspect-brk src/main.js
production:
run with NODE_ENV=production
use process manager or container orchestrator
configure graceful shutdown
12. Common Project Patterns#
repository#
export class InMemoryOrderRepository {
constructor(initialOrders = []) {
this.orders = new Map();
for (const order of initialOrders) {
this.orders.set(order.orderId, order);
}
}
async findById(orderId) {
return this.orders.get(orderId) ?? null;
}
async save(order) {
this.orders.set(order.orderId, order);
return order;
}
}
service#
import { randomUUID } from "node:crypto";
export class OrderService {
constructor(repository) {
this.repository = repository;
}
async createOrder(input) {
const order = {
orderId: randomUUID(),
userId: input.userId,
amount: input.amount,
status: "PENDING",
createdAt: new Date().toISOString()
};
return this.repository.save(order);
}
}
route#
import { Router } from "express";
import { z } from "zod";
export function createOrderRouter(orderService) {
const router = Router();
router.post("/orders", async (req, res, next) => {
try {
const input = z.object({
userId: z.string().min(1),
amount: z.number().positive()
}).parse(req.body);
const order = await orderService.createOrder(input);
res.status(201).json(order);
} catch (error) {
next(error);
}
});
return router;
}
13. Hands-on#
src/app.js#
import express from "express";
import { z } from "zod";
import { randomUUID } from "node:crypto";
const orders = new Map();
export function createApp() {
const app = express();
app.use(express.json());
app.post("/orders", (req, res) => {
const input = z.object({
userId: z.string().min(1),
amount: z.number().positive()
}).parse(req.body);
const order = {
orderId: randomUUID(),
userId: input.userId,
amount: input.amount,
status: "PENDING",
createdAt: new Date().toISOString()
};
orders.set(order.orderId, order);
res.status(201).json(order);
});
app.get("/orders/:orderId", (req, res) => {
const order = orders.get(req.params.orderId);
if (order === undefined) {
res.status(404).json({
error: {
code: "ORDER_NOT_FOUND",
message: "order not found"
}
});
return;
}
res.json(order);
});
return app;
}
src/main.js#
import { createApp } from "./app.js";
const port = Number(process.env.PORT ?? 3000);
const app = createApp();
app.listen(port, () => {
console.log(`order-api listening on port ${port}`);
});
run#
curl -s -X POST http://localhost:3000/orders \
-H 'content-type: application/json' \
-d '{"userId":"u-1001","amount":99.9}'
14. Production Checklist#
project:
Node.js LTS
ESM project
package-lock.json committed
npm ci in CI
lint/test in CI
config:
env config validated at startup
secrets not committed
production config injected by platform
code:
external input validated
async errors handled
clients reused
no sync IO in hot path
no CPU-heavy work on event loop
runtime:
structured logging
request id / trace id
health check endpoint
graceful shutdown
NODE_OPTIONS reviewed