Uploaded by Ridvan Dzh (Ридван Джалилов)

dao-node 1

advertisement
//
Don't structure by technical responsibilities
├── src
|
├── controllers
|
|
├── user.js
|
|
├── catalog.js
|
|
├── order.js
|
├── models
|
|
├── user.js
|
|
├── product.js
|
|
├── order.js
|
├── utils
|
|
|
├── tests
|
|
├── user.test.js
|
|
├── product.test.js
//
├── order.js
Structure by domain modules
├── src
|
├── user
|
|
├── user-handlers.js
|
|
├── user-service.js
|
|
├── user-queries.js
|
|
├── user-handlers.test.js
|
|
├── index.js
|
├── order
|
|
├── order-handlers.js
|
|
├── order-service.js
|
|
├── order-queries.js
|
|
├── order-handlers.test.js
|
|
├── calculate-shipping.js
|
|
├── calculate-shipping.test.js
|
|
├── index.js
|
├── catalog
|
|
├── catalog-handlers.js
|
|
├── product-queries.js
|
|
├── catalog-handlers.test.js
|
|
├── index.js
//
Avoid creating handlers with too many responsibilities
unless their scope is small
const handler = async (req, res) => {
const { name, email } = req.body
if (!isValidName(name)) {
return res.status(httpStatus.BAD_REQUEST).send()
}
if (!isValidEmail(email)) {
return res.status(httpStatus.BAD_REQUEST).send()
}
await queryBuilder('user').insert({ name, email })]
if (!isPromotionalPeriod()) {
const promotionalCode = await queryBuilder
.select('name', 'valid_until', 'percentage', 'target')
.from('promotional_codes')
.where({ target: 'new_joiners' })
transport.sendMail({
// ...
})
}
return res.status(httpStatus.CREATED).send(user)
}
//
Handlers should only handle the HTTP logic
const handler = async (req, res) => {
const { name, email } = req.body;
if (!isValidName(name)) {
return res.status(httpStatus.BAD_REQUEST).send();
}
if (!isValidEmail(email)) {
return res.status(httpStatus.BAD_REQUEST).send();
}
try {
const user = userService.register(name, email);
return res.status(httpStatus.CREATED).send(user);
} catch (err) {
return res
.status(httpStatus.INTERNAL_SERVER_ERROR)
.send();
}
};
// user-service.js
export async function register(name, email) {
const user = await queryBuilder('user').insert({ name, email
})]
if (!isPromotionalPeriod()) {
const promotionalCode = await
promotionService.getNewJoinerCode()
await
emailService.sendNewJoinerPromotionEmail(promotionalCode)
}
return user
}
// user-service.js
export async function register(name, email) {
const user = await userRepository.insert(name, email);
if (!isPromotionalPeriod()) {
const promotionalCode =
await promotionService.getNewJoinerCode();
await emailService.sendNewJoinerPromotionEmail(
promotionalCode
);
}
return user;
}
//
Don't break the boundaries of the domain modules
const placeOrderHandler = (req, res) => {
const { products, updateShippingAddress } = req.body
if (updateShippingAddress) {
// Update the user's default shipping address
const { user } = res.locals.user
const { shippingAddress } = req.body
knex('users').where('id' '=', user.id).update({
shippingAddress })
}
}
//
Communicate using services
const placeOrderHandler = (req, res) => {
const { products, updateShippingAddress } = req.body
if (updateShippingAddress) {
// Update the user's default shipping address
const { user } = res.locals..user
const { shippingAddress } = req.body
userService.updateShippingAddress(user.id, shippingAddress)
}
}
// product-repository.js
//
Avoid returning data directly from storage
// If the storage imposes constraints on naming and formatting.
export async function getProduct(id) {
return dbClient.getItem(id);
}
// product-repository.js
//
Map the retrieved item to a domain object
// Rename storage-specific fields and improve its structure.
export async function getProduct(id) {
const product = dbClient.getItem(id);
return mapToProductEntity(product);
}
GSIPK
GSISK
//
Don't put everything into a utility folder
├── src
|
├── user
|
|
|
├── order
|
|
|
├── catalog
|
|
|
├── utils
|
|
├── calculate-shipping.js
|
|
├── watchlist-query.js
|
|
├── products-query.js
|
|
├── capitalize.js
|
|
├── validate-update-request.js
//
├── ...
├── ...
├── ...
Separate utilities and domain logic
├── src
|
├── user
|
|
├── ...
|
|
├── validation
|
|
|
├── validate-update-request.js
|
├── order
|
|
├── ...
|
|
├── calculate-shipping.js
|
├── catalog
|
|
├── ...
|
|
├── queries
|
|
|
├── products-query.js
|
|
|
├── watchlist-query.js
|
├── utils
|
|
├── capitalize.js
ajv
//
express-validator
Don't validate requests explicitly
const createUserHandler = (req, res) => {
const { name, email, phone } = req.body;
if (
name &&
isValidName(name) &&
email &&
isValidEmail(email)
) {
userService.create({
userName,
email,
phone,
status,
});
}
// Handle error...
};
//
Use a library to validate and generate more descriptive
messages
const schema = Joi.object().keys({
name: Joi.string().required(),
email: Joi.string().email().required(),
phone: Joi.string()
.regex(/^\d{3}-\d{3}-\d{4}$/)
.required(),
});
const createUserHandler = (req, res) => {
const { error, value } = schema.validate(req.body);
// Handle error...
};
// Create a reusable validation middleware
const validateBody = (schema) => (req, res, next) => {
const { value, error } = Joi.compile(schema).validate(
req.body
);
if (error) {
const errorMessage = error.details
.map((details) => details.message)
.join(", ");
return next(
new AppError(httpStatus.BAD_REQUEST, errorMessage)
);
}
Object.assign(req, value);
return next();
};
// Use it in the route definitions
app.put("/user", validate(userSchema), handlers.updateUser);
//
Don't implement business logic in the middleware
const hasAdminPermissions = (req, res, next) => {
const { user } = res.locals
const role = knex
.select('name')
.from('roles')
.where({ 'user_id', user.id })
if (role !== roles.ADMIN) {
throw new AppError(httpStatus.UNAUTHORIZED)
}
next()
}
//
Delegate to a service call
const hasAdminPermissions = (req, res, next) => {
const { user } = res.locals
if (!userService.hasPermission(user.id)) {
throw new AppError(httpStatus.UNAUTHORIZED)
}
next()
}
hasAdminAccess
hasPermissions
//
Don't use a class just for the sake of grouping your logic
class UserController {
updateDetails(req, res) {}
register(req, res) {}
authenticate(req, res) {}
}
//
Use simple handler functions instead
export function updateDetails(req, res) {}
export function register(req, res) {}
export function authenticate(req, res) {}
export function createHandler(logger, userService) {
return {
updateDetails: (req, res) => {
// User the logger and service in here
},
};
}
throw
Error
insteanceof
//
Don't throw plain error messages
const { error } = schema.validate(req.body);
if (!product) {
throw "The request is not valid!";
}
//
Use the built-in Error object
const { error } = schema.validate(req.body);
if (!product) {
throw new Error("The request is not valid");
}
// Extend the built-in Error object
export default class AppError extends Error {
constructor(
statusCode,
message,
isOperational = true,
stack = ""
) {
super(message);
this.statusCode = statusCode;
this.isOperational = isOperational;
if (stack) {
this.stack = stack;
} else {
Error.captureStackTrace(this, this.constructor);
}
}
}
// Use AppError instead
const { error } = schema.validate(req.body);
if (!product) {
throw new AppError(
httpStatus.UNPROCESSABLE_ENTITY,
"The request is not valid"
);
}
isOperational
AppError
ValidationError
InternalServerError
true
process.on("uncaughtException", (err) => {
// Log the exception and exit
});
process.on("SIGTERM", () => {
// Do something and exit
});
//
Don't handle errors on a case-by-case basis
const createUserHandler = (req, res) => {
// ...
try {
await userService.createNewUser(user)
} catch (err) {
logger.error(err)
mailer.sendMail(
configuration.adminMail,
'Critical error occured',
err
)
res.status(500).send({ message: 'Error creating user' })
}
}
//
Propagate the error to a central error handler
const handleError = (err, res) => {
logger.error(err)
sendCriticalErrorNotification()
if (!err.isOperational) {
// Shut down the application if it's not an AppError
}
res.status(err.statusCode).send(err.message)
}
const createUserHandler = (req, res, next) => {
// ...
try {
await userService.createNewUser(user)
} catch (err) {
next(err)
}
}
app.use(async (err, req, res, next) => {
await handleError(err, res)
})
process.on('uncaughtException', (error) => {
handleError(error)
})
Error
app.use((err, req, res, next) => {
if (!err) {
next(new AppError(httpStatus.NOT_FOUND, "Not found"));
}
});
process.on("uncaughtException", (error) => {
handleError(error);
if (!isOperational(error)) {
process.exit(1);
}
});
// 1. Use all caps for constants
const CACHE_CONTROL_HEADER = "public, max-age=300";
// 2. Use camel case for functions and variables
const createUserHandler = (req, res) => {};
// 3. Use pascal case for classes and models
class AppError extends Error {}
// 4. use kebab case for files and folders -> user-handler.js
calculateOrder
├── order
|
├── order-handlers.js
|
├── order-service.js
|
├── order-queries.js
|
├── order-handlers.test.js
|
├── calculate-shipping.js
|
├── calculate-shipping.test.js
|
├── get-courier-cost.js
|
├── calculate-packaging-cost.js
|
├── calculate-discount.js
|
├── is-free-shipping.js
|
├── index.js
index.js
├── order
|
├── order-handlers.js
|
├── order-service.js
|
├── order-queries.js
|
├── order-handlers.test.js
|
├── calculate-shipping
|
|
├── index.js
|
|
├── calculate-shipping.js
|
|
├── calculate-shipping.test.js
|
|
├── get-courier-cost.js
|
|
├── calculate-packaging-cost.js
|
|
├── calculate-discount.js
|
|
├── is-free-shipping.js
|
├── index.js
//
Don't export at the bottom of the file
const createUserHandler = (req, res) => {
// ...
};
const getUserDetailsHandler = (req, res) => {
// ...
};
export { createUserHandler, getUserDetailsHandler };
//
Export together with the definition
export const createUserHandler = (req, res) => {
// ...
};
export const getUserDetailsHandler = (req, res) => {
// ...
};
// Register the main routes
const router = express.Router();
router.use("/user", jobs);
app.use("/v1", router);
// user-routes.js
router
.route("/")
.get(isAuthenticated, handler.getUserDetails);
router
.route("/")
.put(
isAuthenticated,
validate(userDetailsSchema),
handler.updateUserDetails
);
app.use("/v1", routes);
res.locals
res.locals
Record<string, any>
req
req.user
res.locals
Request
Response
.then()
await
//
Do not use callback-based APIs to avoid deep nesting
import fs from "node:fs";
fs.open("./some/file/to/read", (err, file) => {
// Handle error...
// Do something with the file...
fs.close(file, (err) => {
// Handle closing error...
});
});
//
Use promise-based APIs
import { open } from "node:fs/promises";
const file = await open("./some/file/to/read");
// Do something with the file...
await file.close();
try catch
.catch()
//
Don't create modules with shallow interfaces
export function validateUserDetails(user) {
// ...
}
export function checkEmailTaken(email) {
// ...
}
export async function storeUser(user) {
// ...
}
export function sendVerificationEmail(user) {
// ...
}
//
Create deep interfaces
function validateUserDetails(user) {
// ...
}
function checkEmailTaken(email) {
// ...
}
async function storeUser(user) {
// ...
}
function sendVerificationEmail(user) {
// ...
}
export function verifyAndCreateUser(userData) {
const err = validateUserDetails(userData);
if (err.isValid) {
throw new Error(err.message);
}
const emailTaken = checkEmailTaken(userData.email);
if (emailTaken) {
throw new Error("Email already taken");
}
try {
const user = await storeUser(user)
} catch(err) {
throw new Error("Error storing user");
}
sendVerificationEmail(user);
}
validateAndCreateUser
createUser
//
Don't use environment variables directly
function createUserHandler(user) {
// ...
if (process.env.ANALYTICS_ENABLED) {
analyticsService.trackEvent(
"User",
"Created",
user.email
);
}
// ...
}
//
Pass them down as configuration and make it obvious that
they are used
function createHandler(analyticsEnabled) {
return {
createUser: function (user) {
// ...
if (analyticsEnabled) {
analyticsService.trackEvent(
"User",
"Created",
user.email
);
}
// ...
},
};
}
function createHandler({ analyticsService }) {
return {
createUser: function (user) {
// ...
if (analyticsService) {
analyticsService.trackEvent(
"User",
"Created",
user.email
);
}
// ...
},
};
}
// Example of usage
createHandler({
analyticsService: process.env.ANALYTICS_ENABLED
? analyticsService
: undefined,
});
//
Avoid changing a reusable function because of a single
case.
// User Module
calculateDiscount(user);
// Order Module
calculateDiscount(user);
// Cart Module
calculateDiscount(user);
// Promotions Module
calculateDiscount(user, promotionCode);
function calculateDiscount(user, promotionCode) {
if (promotionCode) {
// Handle specific case...
}
// ...
}
//
Duplicate some of the code if necessary or inline it
function calculateDiscount(user) {
// ...
}
function calculatePromotionCodeDiscount(
user,
promotionCode
) {
// ...
}
function makeSavingThrow(character, savingThrow) {
const hasAdvantage = character.hasAdvantage(
savingThrow.ability
);
const rollCount = hasAdvantage ? 2 : 1;
const roll = createRoll(`${rollCount}d20`).getResult();
const abilityModifier = Math.round(
(character.abilities[savingThrow.ability] - 10) % 2
);
const isSuccess =
roll + abilityModifier >=
savingThrow.getSuccessThreshold();
return isSuccess;
}
Array.prototype.map
//
Descriptive variables are not enough
const JULY = 6;
if (paymentDate.getMonth() >= JULY) {
// ...
}
//
Improving the name may still lead to confusion
const FINANCIAL_YEAR_START_MONTH = 6;
if (paymentDate.getMonth() >= FINANCIAL_YEAR_START_MONTH) {
// ...
}
//
Describe business specifics in a comment
// We follow the Australian financial year and it starts in July
if (paymentDate.getMonth() > July) {
// ...
}
/**
* Checks if `value` is an `Error`, `EvalError`, `RangeError`,
`ReferenceError`,
* `SyntaxError`, `TypeError`, or `URIError` object.
*
* isError(new Error)
* // => true
*
* isError(Error)
* // => false
*/
export function isError(value) {
// ...
}
//
Don't use comments to outline the logical parts of a
function
function createUserAccount(email, password) {
// Validate if the email is vacant.
// ...
// Create the new user entity.
// ...
// Send an account confirmation email.
// ...
}
//
Favor more descriptive code
function createUserAccount(email, password) {
validateEmail(email);
createUserEntity(email, password);
sendConfirmationEmail(email);
}
//
This comment provides context, but there's room for
improvement.
// make a deep copy to to avoid changes by reference
const data = JSON.decode(JSON.encode(response));
//
By wrapping it in a function we can make our code more
descriptive.
function makeDeepCopy(response) {
return JSON.decode(JSON.encode(response));
}
this
// There isn't anything inherently wrong with a class, but we can
simplify it
class AnalyticsService {
constructor(entity, url) {
this.entity = entity;
this.url = url;
}
trackEvent(eventName, eventData) {
// ...
}
trackEventWithMetrics(eventName, eventData, metrics) {
// ...
}
}
const userAnalyticsService = new AnalyticsService(
"User",
"https://example.com"
);
//
Favor factory functions
function createAnalyticsService(entity, url) {
return {
trackEvent(eventName, eventData) {
// ...
},
trackEventWithMetrics(eventName, eventData, metrics) {
// ...
},
};
}
const userAnalyticsService = createAnalyticsService(
"User",
"https://example.com"
);
//
Avoid relying on specific implementations
function createAnalyticsService(url) {
const httpClient = axios.create({ baseURL: url });
return {
trackEvent(eventName, eventData) {
httpClient.post();
},
trackEventWithMetrics(eventName, eventData, metrics) {
httpClient.post();
},
};
}
const userAnalyticsService = createAnalyticsService(
"https://example.com"
);
//
Accept them as arguments to make them visible
function createAnalyticsService(httpClient) {
return {
trackEvent(eventName, eventData) {
httpClient.post();
},
trackEventWithMetrics(eventName, eventData, metrics) {
httpClient.post();
},
};
}
const axiosClient = axios.create({
baseURL: "http://example.com",
});
const userAnalyticsService =
createAnalyticsService(axiosClient);
interface HTTPClient<T> {
post: (url: string) => Promise<T>;
}
function createAnalyticsService(httpClient:
HTTPClient<AnalyticsResponse>) {
return {
trackEvent(eventName, eventData) {
httpClient.post(...);
},
trackEventWithMetrics(eventName, eventData, metrics) {
httpClient.post(...);
},
};
}
createEntity
//
The UserRepository inherits all methods and it overrides
the ones it doesn't support
class Repository {
constructor(dbClient) {
this.dbClient = dbClient;
}
getEntities(filters) {
return this.dbClient.where(filters).get();
}
getEntity(id) {
return this.dbClient.where({ id }).get();
}
updateEntity(id, data) {
return this.dbClient.where({ id }).update(data);
}
deleteEntity(id) {
return this.dbClient.where({ id }).delete();
}
}
class UserRepository extends Repository {
constructor(dbClient) {
super(dbClient);
}
getEntities(filters) {}
updateEntity(id, data) {}
deleteEntity(id) {}
}
//
With composition, the repository can only add the functions
it needs
function canRetrieveSingleEntity(dbClient) {
return {
getEntity: (id) => dbClient.where({ id }).get(),
};
}
function createUserRepository(dbClient) {
return {
...canRetrieveSingleEntity(dbClient),
};
}
typeof
object
//
A function with multiple return types introduces complexity
function promotionalProductsHandler(req, res) {
const productsResult = service.getPromotionalProducts();
if (!productsResult) {
return res.json({ products: [] });
}
if (!Array.isArray(productsResult)) {
return res.json({ products: [productsResult] });
}
return res.json({ products: productsResult });
}
//
By always returning a collection we remove all the edge
cases
function promotionalProductsHandler(req, res) {
const productsResult = service.getPromotionalProducts();
return res.json({ products: productsResult });
}
//
Avoid using boolean flags in functions
distance(startingPoint, destinationPoint, true);
//
Instead, split the logic in two separate functions
distanceInKilometers(startingPoint, destinationPoint);
distanceInMiles(startingPoint, destinationPoint);
// It's not clear what the flag means
function area(points, isMiles) {}
// The check is for miles but the call looks the same like the
one above
area(points, true);
enum Unit {
Kilometer,
Mile,
Hectare
}
function distance(from: Point, to: Point, units: Unit) {
// ...
}
//
Avoid passing unclear parameters to functions
createUser(sanitizeInput(req.body), {}, "customer");
//
Add more context even if it makes your code more verbose
const sanitizedUserAttributes = sanitizeInput(req.body);
const options: Options = {};
const userType: UserType = "customer";
createUser(sanitizedUserAttributes, options, userType);
//
Avoid inlining conditional statements with multiple checks
if (
user.isActive &&
user.type === UserType.Admin &&
user.companyID === product.companyID
) {
// ...
}
//
Encapsulate the checks in a function with a descriptive
name
if (canEditProduct(user, product)) {
// ...
}
//
Avoid nested conditional checks
if (user.isActive) {
if (user.type !== UserType.Admin) {
if (user.companyID === product.companyID) {
// ...
} else {
throw new Error(
"User cannot edit product from a different company"
);
}
} else {
throw new Error("User is not an admin");
}
} else {
throw new Error("User is not active");
}
//
Invert the conditional statements
if (!user.isActive) {
throw new Error("User is not active");
}
if (user.type !== UserType.Admin) {
throw new Error("User is not an admin");
}
if (user.companyID !== product.companyID) {
throw new Error("User is not an admin");
}
// ...
//
Avoid conditinoal chains
function getProductDiscount(product) {
switch (product.type) {
case "book":
return 0.05;
case "clothing":
return 0.1;
case "games":
return 0.05;
}
}
//
Implement the functionality inside the entity itself
product.getDicount();
EventEmitter
events
//
Avoid using events across your whole application
// user-service.js
const user = await createUser({ ... });
eventEmitter.emit("user-created", user);
// email-service.js
eventEmitter.on("user-created", sendWelcomeEmail);
// promotion-service.js
eventEmitter.on("user-created", enableFreeTrial);
//
Call the functions manually to increase visibility
const onUserCreatedListeners = [
emailService.sendWelcomeEmail,
promotionService.enableFreeTrial,
];
// ...
const user = await createUser({ ... });
onUserCreatedListeners.forEach((listener) => listener(user));
fs
export interface Logger {
log: LogFunction;
warn: LogFunction;
error: LogFunction;
}
type LogFunction = (...args: any[]) => void;
let logger: Logger = console;
export function setLogger(customLogger: Logger) {
logger = customLogger;
}
export default logger;
.then
async/await
async/await
async/await
async/await
try/catch
.catch
try/catch
catch
await
Promise.all
Promise.race
try/catch
try/catch
//
Don't wrap an entire function in a try-catch block
const createUserHandler = async (req, res) => {
try {
const { email, password, passwordConfirmation } =
req.body;
const validationResult = isValidUser(email, password);
if (!validationResult.isValid) {
return res
.status(422)
.send({ error: validationResult.message });
}
if (password !== passwordConfirmation) {
return res
.status(422)
.send({ error: "Passwords don't match" });
}
const user = {
email,
password,
};
if (getCurrentDate() < getPromotionPeriodEndDate()) {
// Give the user a free one-month subscription
user.subscriptionEndDate = getDateAfterMonths(1);
}
await userService.create(user);
emailService.sendWelcomeEmail(user);
return res
.status(201)
.json({ message: "User created" });
} catch (err) {
logger.err(err);
return res.status(500).json({ message: err.message });
}
};
//
Wrap only the parts of the function that need to be
protected
const createUserHandler = async (req, res) => {
// ...
try {
await userService.create(user);
} catch (err) {
logger.err(err);
return res.status(500).json({ message: err.message });
}
try {
emailService.sendWelcomeEmail(user);
} catch (err) {
// We don't want to return an error if the email fails
logger.err(err);
}
// ...
};
try/catch
//
The naming around this is clear only if you're familiar
with the code.
const users = userService.getUsers({ type: "author" });
const data = users.reduce((curr, acc) => {
return {
...acc,
[curr.name]: curr.articles.map((x) => x.title),
};
}, {});
//
Add more context to the names of the variables.
const authors = userService.getAuthorsAndLatestArticles();
const articleNamesByAuthor = authors.reduce(
(map, author) => {
return {
...map,
[author.name]: author.articles.map(
(article) => article.title
),
};
},
{}
);
data
helper
Options
Context
lodash
underscore
Object.entities
Object.values
Array.fill
flatMap
Object.find
Array.filter
Object.keys
Array.from
Array.map
Array.concat
console.log
winston
winston
fastify
pino
~
^
docker-compose
const config = {
environment: process.env.NODE_ENV,
port: process.env.PORT,
};
export default config;
config
//
The object name already holds the context
const user = {
userName: "...",
userEmail: "...",
userAddress: "...",
};
//
Remove the unnecessary prefix
const user = {
name: "...",
email: "...",
address: "...",
};
const config = {
storage: {
bucketName: process.env.S3_BUCKET_NAME,
},
database: {
name: process.env.DB_NAME,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
},
};
export default config;
import/export
require
import/export
import/export
require
require
function createLogger() {
const logger = pino();
return {
info: (message) => logger.info({ message }),
warn: (message) => logger.warn({ message }),
error: (message) => logger.error({ message }),
fatal: (message) => logger.fatal({ message }),
};
}
function createLogger() {
const logger = pino();
return enrichLogger(logger);
}
function enrichLogger(logger) {
return {
info: (message) => logger.info({ message }),
warn: (message) => logger.warn({ message }),
error: (message) => logger.error({ message }),
fatal: (message) => logger.fatal({ message }),
withTransactionID: (transactionID) =>
enrichLogger(logger.child({ transactionID })),
withEndpoint: (endpoint) =>
enrichLogger(logger.child({ endpoint })),
};
}
// logger.js
const pino = require("pino");
function createLogger(level, transport) {
const logger = pino({ level }, transport);
return enrichLogger(logger);
}
// app.js
const pinoElastic = require("pino-elasticsearch");
const ESTransport = pinoElastic({
index: "an-index",
consistency: "one",
node: "http://localhost:9200",
"es-version": 7,
"flush-bytes": 1000,
});
const logger = createLogger("info", ESTransport);
// metrics-client.js
import client from "prom-client";
const registry = new client.Registry();
colleclient.collectDefaultMetrics({
app: "user-service",
prefix: "user_service_",
timeout: 10000,
gcDurationBuckets: [0.001, 0.01, 0.1, 1, 2, 5],
register: new client.Registry(),
});
export default registry;
// app.js
import metricsClient from "./metrics-client";
const app = express();
app.get("/metrics", async (req, res) => {
res.setHeader("Content-Type", metricsClient.contentType);
res.send(await metricsClient.metrics());
});
// tracer.js
import {
Tracer,
ExplicitContext,
BatchRecorder,
jsonEncoder,
} from "zipkin";
import { HttpLogger } from "zipkin-transport-http";
import zipkinInstrumentation from "zipkin-instrumentationexpress";
import config from "./config";
const tracer = new Tracer({
ctxImpl: new ExplicitContext(),
recorder: new BatchRecorder({
logger: new HttpLogger({
endpoint: `${config.zipkinEndpoint}/api/v2/spans`,
jsonEncoder: jsonEncoder.JSON_V2,
}),
}),
localServiceName: config.serviceName,
});
export default zipkinInstrumentation.expressMiddleware({
tracer,
});
// app.js
import tracer from "./tracer";
const app = express();
app.use(tracer);
// http-client.js
import axios from "axios";
import zipkinInstrumentationAxios from "zipkin-instrumentationaxios";
import tracer from "./tracer";
import config from "./config";
export default zipkinInstrumentationAxios(axios, {
tracer,
serviceName: config.serviceName,
});
// http-client.js
import axios from "axios";
import zipkinInstrumentationAxios from "zipkin-instrumentationaxios";
import tracer from "./tracer";
export default function createHTTPClientWithTracing(
client,
config
) {
return zipkinInstrumentationAxios(axios, {
tracer,
serviceName: config.serviceName,
});
}
--noverify
// flags-client.js
import { startUnleash } from "unleash-client";
import config from "./config";
const unleash = await startUnleash({
appName: config.applicationName,
url: config.unleash.URL,
customHeaders: {
Authorization: config.unleash.APIToken,
},
});
export default unleash;
// user-service.js
import flagsClient from "../flags-client";
// ...
if (flagsClient.isEnabled("new-analytics-store")) {
// ...
}
forever
pm2
describe("User Service", () => {
it("Should create a user given correct data", async () => {
// 1. Arrange - prepare the data, create any objects you need
const mockUser = {
// ...
};
const userService = createUserService(
mockLogger,
mockQueryBuilder
);
// 2. Act - execute the logic that you're testing
const result = userService.create(mockUser);
// 3. Assert - validate the expected result
expect(mockLogger).toHaveBeenCalled();
expect(mockQueryBuilder).toHaveBeenCalled();
expect(result).toEqual(/** ... */);
});
});
axios
supertest
axios
faker
supertest
dataloader
├── src
|
├── index.js
|
├── resolvers.js
|
├── typeDefs.js
|
├── User
|
|
├── index.js
|
|
├── mutations.js
|
|
├── queries.js
|
|
├── resolvers.js
|
|
├── types.js
|
├── Product
|
|
├── ...
deprecated
#
Avoid generic queries
query GetComparedProducts($ids: [ID!]) {
products(ids: $ids) {
# ...
}
}
#
Favor specific queries
query GetComparedProducts($mainProduct: ID!, $comparedProduct:
ID!) {
getComparedProducts(mainProduct: $mainProduct, comparedProduct:
$comparedProduct) {
mainProduct {
# ...
}
comparedProduct {
# ...
}
}
}
#
Avoid generic mutations
sendEmail(type: PASSWORD_RESET)
#
Favor specific ones
sendPasswordResetEmail()
#
Avoid mutations with multiple parameters
updateAccount(id: 4, newEmail: ...) {
# ...
}
#
Favor mutations with a single object parameter
updateAccount(input: { id: 4, newEmail: ... })
#
Avoid queries with unnested properties
getArticle(id: $id, title: $title) {
id
url
label
slug
publisher
headline
kicker
flags
}
#
Favor queries that nest data in objects
getArticle(id: $id, title: $title) {
article {
id
url
label
slug
publisher
headline
kicker
flags
}
}
const searchHandler = async (req: Request, res: Response) => {
const { term } = req.query
DBClient.update({
TableName: 'users',
Item: {
...
RecentlySearchedTerms: [term]
}
})
const results = await DBClient.query({
...
})
}
import accountsService from '@modules/accounts/service'
const searchHandler = async (req: Request, res: Response) => {
const { term } = req.query
accountsService.updateRecentlySearched(term)
const results = await DynamoDBClient.query({ ... })
}
// modules/accounts/service.ts
const accountsService = {
updateRecentlySearched: (term) => {
return dynamoDBClient.put({
TableName: 'users',
Item: {
...
RecentlySearchedTerms: [term]
}
})
}
}
// modules/accounts/service.ts
const createAccountsService = () => {
const client = axios.create({
baseURL: ACCOUNTS_SERVICE_URL,
timeout: 1000,
headers: { ... }
});
return {
updateRecentlySearched: (term) => {
return client.post('/recent-searches', { term })
.catch(err => {
// Handle the error
})
}
}
}
Download