// 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 }) } } }