└── README.md /README.md: -------------------------------------------------------------------------------- 1 | # Typescript-Best-Practices 2 | Patterns and Best Practices for procedural Typescript/JavaScript development following the rule of 4 principle 3 | 4 | 5 | ## Table of contents 6 | - [4 fundamental features](#4-fundamental-features) 7 | - [4 types of scripts](#4-types-of-scripts) 8 | - [Script (file) Organization](#script-organization) 9 | - [4 fundamental features in detail](#4-fundamental-features-in-detail) 10 | - [Primitives](#primitives) 11 | - [Functions](#functions) 12 | - [Objects](#objects) 13 | - [Object-literals](#object-literals) 14 | - [Classes](#classes) 15 | - [Enums](#enums) 16 | - [Types](#types) 17 | - [Naming](#naming) 18 | - [Files/Folders](#files-folders) 19 | - [General Notes](#general-naming-notes) 20 | - [Functions](#naming-functions) 21 | - [Objects](#naming-objects) 22 | - [Types](#naming-types) 23 | - [Comments](#comments) 24 | - [Imports](#imports) 25 | - [Example Scripts](#example-scripts) 26 | - [Misc Style](#misc-style) 27 | - [Testing](#testing) 28 | - [General Notes](#testing-general-notes) 29 | - [Structuring BDD style tests](#testing-bdd-style) 30 |
31 | 32 | 33 | ## 4 "Fundamental" Features 34 | - `Primitives`, `Functions`, `Objects`, and `Types` 35 | - Primitives - 5 original: `null`, `undefined`, `boolean`, `number`, `string`. Two new ones `symbol` and `bigint`. 36 | - Functions - 4 ways to create functions: function-declarations `function functionName() {}`, arrow-functions `() => {}`, placing them directly in object-literals (not counting arrows), and directly inside classes (not counting arrows). 37 | - Objects - 4 ways to create objects: object-literals, enums, classes, calling functions with `new` (obsolete es5 way). 38 | - Types - 2 main ways to create types: types-aliases (`type`) and interfaces (`interface`). Note: there's also function overloading for function-declarations. 39 | - Note: Functions are technically objects too but for all practical purposes we'll consider them separate. 40 |
41 | 42 | 43 | ## 4 types of scripts 44 | - Declaration: exports one large declared item (i.e. a file called HttpStatusCodes.ts which exports a single enum containing all the http status codes. 45 | - Modular-Object: In JavaScript, module is another term for file, so if we use a single object to represent the items available from that file, we'll call it a modular-object script. The export default is an `object-literal` containing a bunch of closely related functions/variables (i.e. a file call UserRepo.ts which has a bunch of functions for handling database queries related to user objects, we refer to it as the _UserRepo_ module). 46 | - Inventory: for storing a large number of smaller declared items. (i.e. a file called types.ts which stores commonly shared types throughout your application) 47 | - Linear: executes a series of commands (i.e. a file called `setup-db.ts` which does a bunch of system commands to initialize a database). 48 |
49 | 50 | 51 | ## Script (file) Organization 52 | - Because of how hoisting works in JavaScript, you should organize a file into these regions. Note that your file may not (and usually won't) have all of them: 53 | - Constants 54 | - Types 55 | - Run/Setup (Special Note: execute any logic that you need to here. Outside of linear scripts you usually shouldn't need this region, but if you do keep it short). 56 | - Functions 57 | - Export 58 | - Some special notes about organization: 59 | - Only constant/readonly variables (primitive and object values) should go directly in files in the `Constants` region (except maybe in linear scripts). 60 | - If you are writing a linear script, it might make more sense to group your code by the task they are doing instead of by the fundamental-feature. Still, if you decide to create some function-declarations in the same script, place your function-declarations in another region at the bottom of the file below the Run. 61 | - Always put the `export default` at the very bottom of every file. This makes your default export easier to keep track of and apply any wrappers it may need. 62 | - Organzation overview 63 | - Project (application or library) 64 | - Directory (Folder) 65 | - File (aka module) 66 | - Region 67 | - Section 68 |
69 | 70 | 71 | ## 4 fundamental features in detail and when/how you should use them. 72 | 73 | ### Primitives 74 | - To repeat: the 5 original are: `null`, `undefined`, `boolean`, `number`, `string` and the two new ones added recently are `symbol` and `bigint`. 75 | - `symbol` is not nearly as prevalent as the others but is useful for creating unique keys on objects used in libraries. Since libraries in objects are passed around a lot, with symbols we don't have to worry about our key/value pairs getting overridden. 76 | - In addition to knowing what the primitives are you should know how coercion works. Coercion is when we try to call a function on a primtive and JavaScript (under the hood) wraps its object counterpart (`Boolean`, `Number`, or `String`) around it so we can make the function call, since primitives by themselves don't have functions. 77 | 78 | ### Functions 79 | - As mentioned, there are function-declarations made with `function fnName() {}` and arrow-functions made with `() => {}`. Function-declarations should be used directly in files (so that we can use hoisting) and arrow functions should be used when creating functions inside of other functions (i.e. a callback passed to a JSX element). You may have to make exceptions to this when working with certain libraries cause of how `this` works in each, but generally this is how it should be done. 80 | - When using arrow functions, only use parenthesis for the params if there are multiple params. Paranthesis are overkill if there is only one param: 81 | ```.ts 82 | function ParentFn(param) { 83 | const childFn = val => ...do something with the val; 84 | const childFn2 = (val1, val2) => do something else; 85 | const badThing = (val) => ...do something else with the val; 86 | childFn(val); 87 | } 88 | ``` 89 | - Functions can be be placed directly in object literals. I usually do it this way for longer multi-line functions but will use an arrow function for short functions. Note that for `direct-in-object-literal` functions The `this` keyword will refer to properties on the containing object. 90 | ``` 91 | const objLiteral = { 92 | helloPre: 'hello ', 93 | sayHello(name: string) { 94 | console.log(this.helloPre + name); 95 | }, 96 | sayHelloAlt: (name: string) + console.log(...) // Can also use an arrow function but the `this` keyword won't refer to the parent-object literal. 97 | } 98 | ``` 99 | 100 | ### Objects 101 | - _Objects_ are lists of key/value pairs and there are 3 templates for intializing objects, object-literals, enums, and classes. 102 | - Another way to initialize objects is to call a function with `new` but this is considered obsolete next to classes and leads to more complex code when doing inheritance. 103 | - `instance-objects` are objects created from classes or calling functions with `new`. 104 | - All objects inherit from the parent `Object` class. We'll use the term basic-object to refer to objects which inherit directly from this class and no other. 105 | - Just to point out, symbols have single key/value pair and functions also have key/values pairs and inherit from the `Function` class which in turn inherits from the `Object` class. Due to how we use these features though, we'll consider objects, functions, and symbols separate datatypes. Also note that in Javascript objects are dynamic (we can append as many properties as we want) but in Typescript the keys are static by default once the object is instantiated. 106 | 107 | #### Object-literals 108 | - `object-literals` are a what's created from doing key/value pairs lists with curly-braces (ie `{ name: 'john' }`) and are a convenient, shorthand way of creating basic-objects. They make it easy to both organize code and pass data around. 109 | - When we use `export default { func1, func2, etc} as const` at the bottom of a modular-object script, we are essentially using object-literals to organize our code. 110 | - We should generally (but not always) use object-literals over classes for organizing code for the reasons mentioned in the next section. 111 | 112 | #### Classes 113 | - **Overview:** The trend in JavaScript nowadays is to move away from classes to organize our code and switch to procedural/functional programming. This means the backbone of our application is simpler and we don't have to worry about dependency-injection or making constructor calls on every single data item when working with IO data. It's better to organize our code using modular-object instead of classes; however, there are still some scenarios where it might make sense to use classes. Let's look at these points in more detail. 114 | - **When not to use classes:** 115 | - When only one instance of something is needed: Dependency-injection is what we mean when we're trying to use the same instance of an object in several places. If we use classes for organizing the portions of our code where multiple instances aren't needed (i.e. a web servers "Service" layer), we'd have use dependency-injection by marking every function `public static` and using it directly on the class itself, or making sure to instantiate the class before we export it (i.e. `export default new UserServiceLayer()`, note that all exports are singletons), and finally another option would be to use a special library for dependency-injection (i.e. `TypeDI`). 116 | - Serializable Data: Using classes as templates for serializable data could get a little messy as well. The reason for this is when retrieving objects from any kind of IO call, our key/value pairs are what gets transferred in that call, not the functions themselves or the prototype pointers. In order to use the functions or the `instanceof` keyword, we'd have to pass all our data-instances through a constructor or declare the functions static and use them directly from our class (i.e. do `public static toString()` in the `User` class and call User.toString("some data item") or wrap `new User()` around every data-item). It'd be better just to leave the data-item as a basic-object and describe it with an `interface`.
117 | - **When to use classes:** We should only use classes when we have functions and non-serializable data that needs to be tightly coupled together AND we are creating multiple instances of that data. Some examples of this are data-structures (i.e. `new Map()`) or a library that needs to be passed some settings from a user. In general, try to follow the "M.I.N.T." principle, which stands for Multiple Instances, Not-Serialized, and Tightly-Coupled (data coupled with functions). 118 | - **Decoupling data from classes to follow the MINT principle:** For a serializable data-item, what I usually do is a create a modular-object script to represent just that data-item (i.e. User.ts) and in there I'll have the interface (to describe the data) and functions to handle logic related to it (i.e. `new()` and `test()`).
119 | ```ts 120 | // User.ts (to handle IO data) 121 | interface IUser { 122 | id: number; 123 | name: string; 124 | } 125 | 126 | function test(arg: unknown): arg is IUser { 127 | return typeof arg === 'object' && 'id' in arg && 'name' in arg; 128 | } 129 | 130 | function toString(user: IUser): string { 131 | return `Id: "${user.id}", Name: "${user.name}"`; 132 | } 133 | 134 | export default { 135 | test, 136 | toString, 137 | } as const; 138 | ``` 139 | 140 | ```ts 141 | // UserService.ts (to avoid dependency injection) 142 | import User, { IUser } from 'models/User'; 143 | 144 | async function fetchAndPrint(): Promise { 145 | const resp = await someIoCall(), 146 | dataItem = resp.data; 147 | if (User.test(dataItem)) { 148 | console.log(User.toString(dataItem)); 149 | } 150 | } 151 | 152 | export default { 153 | fetchAndPrint, 154 | } as const; 155 | ``` 156 | 157 | #### Enums 158 | Enums are somewhat controversial, I've heard a lot of developers say they do and don't like them. I like enums because because we can use the enum itself as a type which represents and `OR` for each of the values. We can also use an enum's value to index that enum and get the string value of the key. Here's what I recommend, don't use enums as storage for static values, use a readonly object for that with `as const`. Use enums for when the value itself doesn't matter but what matters is distinguishing that value from related values. For example, suppose there are different roles for a user for a website. The string value of each role isn't important, just that we can distinguish `Basic` from `SuperAdmin` etc. If we need to display the role in a UI somewhere, we can change the string values for each role without affecting what's saved in a database. 159 | ```typescript 160 | // Back and front-end 161 | enum UserRoles { 162 | Basic, 163 | Admin, 164 | Owner, 165 | } 166 | 167 | // Front-end only 168 | const UserRolesDisplay = { 169 | [UserRoles.Basic]: 'Basic', 170 | [UserRoles.SAdmin]: 'Administrator', 171 | [UserRoles.Owner]: 'Owner' 172 | } as const; 173 | 174 | interface IUser { 175 | role: UserRoles; // Here we use the enum as a type 176 | } 177 | 178 | function printRole(role: UserRoles) { 179 | console.log(UserRolesDisplay[role]); // => "Basic", "Administrator", "Owner" 180 | } 181 | ``` 182 | 183 | ### Types (type-aliases and interfaces) 184 | - Use interfaces (`interface`) by default for describing simple structured key/value pair lists. Note that interfaces can be used to describe objects and classes. 185 | - Use type-aliases (`type`) for everything else. 186 |
187 | 188 | 189 | ## Naming 190 | 191 | ### Files/Folders 192 | 193 | #### Misc Notes 194 | - Folders: Generally use lowercase with hyphens, but you can make exceptions for special situations (i.e. a folder in React holding Home.tsx and Home.test.tsx could be named `Home/`. 195 | - Declaration scripts: filename should match declaration name. (i.e. if the export default is a function `useSetState()`, the filename should be `useSetState.ts`. 196 | - Modular-object scripts: PascalCase. 197 | - Inventory: lowercase with hyphens (shared-types.ts) 198 | - Linear: lowercase with hyphens (setup-db.ts) 199 | - Folders not meant to be committed as part of the final code, but may exists along other source folders, (i.e. a test folder) should start and end with a double underscore `__test-helpers__`. 200 | 201 | #### The "common/" and "support/" subfolders 202 | - Try to avoid naming folders `misc/` or `shared/`. These can quickly become dumping grounds for all kinds of miscellaneous content making your code disorganized. What I usually do is, if a folder has files with shared content, create a subfolder named `common/` which will only ever have these three subfolders `constants/`, `utils/` and `types/`. You can create multiple `common/` folders for different layers/sections of your application and remember to place each one's content only at the highest level that it needs to be. Here's a list of what each `common/` subfolder is for: 203 | - `utils/`: logic that needs to be executed (i.e. standalone functions, modular-object scripts, and classes) 204 | - `constants/`: static items, could be objects, arrays, or primitives 205 | - `types/`: for type aliases (i.e. custom utility types) and interfaces 206 | - CHEAT: If you have a very simple `common/` folder, that only has a single file that's a declaration or modular-object script, you can have just that one file in there without creating the `constants/`, `utils/` and `types/` subfolders, but remember to add these if that `common/` folder grows though. 207 | - In short `common/` is not a grab-n-bag, `common/` is ONLY for shared types, constants, and utilities (executable logic) that are used across multiple files, nothing else. 208 | - If you have something that isn't shared but you don't want it to go in the file that it is used in for whatever reason (i.e. a large function in an express route that generates a PDF file) create another subfolder called `support/`and place it there. 209 | 210 | #### Example of file/folder naming using a basic express server 211 | ``` 212 | - src/ 213 | - common/ 214 | - constants/ 215 | - HttpStatusCodes.ts 216 | - Paths.ts 217 | - index.ts 218 | - types/ 219 | - index.ts 220 | - utils/ 221 | - StringUtil.ts 222 | - misc.ts // A bunch of standalone functions 223 | - routes/ 224 | - common/ 225 | - Authenticator.ts // See cheat above. Authenticator.ts is a modular-object script that would never be used outside of routes/. 226 | - support/ 227 | - generateUserPaymentHistoryPdf.ts // A single function used only in UserRoutes.ts but is large/complex enough to have it's own file. 228 | - UserRoutes.ts 229 | - LoginRoutes.ts 230 | - server.ts 231 | - tests/ 232 | - common/ 233 | - tests/ 234 | - user.tests.ts 235 | - login.tests.ts 236 | ``` 237 | 238 | ### General Notes 239 | - Primitives should be declared at the top of files at the beginning of the "Constants" section and use UPPER_SNAKE_CASE (i.e. `const SALT_ROUNDS = 12`). 240 | - Readonly arrays/objects-literals (marked with `as const`) should also be UPPER_SNAKE_CASE. 241 | - Variables declared inside functions should be camelCase, always. 242 | - Boolean values should generally start with an 'is' (i.e. session.isLoggedIn) 243 | - Use `one-var-scope` declarations for a group of closely related variables. This actually leads to a slight increase in performance during minification. DON'T overuse it though. Keep only the closely related stuff together. 244 | ```typescript 245 | // One block 246 | const FOO_BAR = 'asdf', 247 | BLAH = 12, 248 | SOMETHING = 'asdf'; 249 | 250 | // Auth Paths 251 | const AUTH_PATHS = [ 252 | '/login', 253 | '/signup', 254 | ] as const; 255 | 256 | // Errors, don't merge this with AUTH_PATHS 257 | const ERRS = { 258 | Foo: 'foo', 259 | Bar: 'bar', 260 | } as const; 261 | ``` 262 | 263 | ### Functions 264 | - camelCase in most situtations but for special exceptions like jsx elements can be PascalCase. 265 | - Generally, you should name functions in a verb format: (i.e. don't say `name()` say `getName()`). 266 | - For functions that return data, use the `get` word for non-io data and fetch for IO data (i.e. `user.getFullName()` and `UserRepo.fetchUser()`). 267 | - Simple functions as part of object-literals just meant to return constants don't necessarily need to be in a verb format. Example: 268 | ```typescript 269 | const ERRORS = { 270 | SomeError: 'foo', 271 | EmailNotFound(email: string) { 272 | return `We're sorry, but a user with the email "${email}" was not found.`; 273 | }, 274 | } as const; 275 | 276 | // Note: Errors in an immutable basic-object because we create it with an object-literal and make it immutable with 'as const'. 277 | ``` 278 | - Prepend helper functions (function declarations not meant to be used outside of their file) with an underscore (i.e. `function _helperFn() {}`). 279 | - If you want to name a function that's already a built-in keyword, pad the name with a double underscore `__`: 280 | ``` 281 | // User.ts 282 | 283 | function __new__(): IUser { 284 | return ...do stuff 285 | } 286 | 287 | export default { new: __new__ } as const; 288 | ``` 289 | 290 | ### Objects 291 | - Generally, objects initialized outside of functions and directly inside of files with object-literals should be immutable (i.e. an single large `export default {...etc}` inside of a Colors.ts file) and should be appended with `as const` so that they cannot be changed. As mentioned in the Variables section, simple static objects/arrays can be UPPER_SNAKE_CASE. However, large objects which are the `export default` of declaration or modular-object scripts should be PascalCase. 292 | - `instance-objects` created inside of functions or directly in the file should use camelCase. 293 | - PascalCase for class names and any `static readonly` properties they have (i.e. Dog.Species). 294 | - Use PascalCase for the enum name and keys. (i.e. `enum NodeEnvs { Dev = 'development'}`) 295 | ```typescript 296 | // UserRepo.ts 297 | 298 | // camelCase cause dynamic object 299 | const dbCaller = initializeDatabaseLibrary(); 300 | 301 | function findById(id: number): Promise { 302 | dbCaller.query()... 303 | } 304 | 305 | function findByName(name: string): Promise { 306 | dbCaller.query()... 307 | } 308 | 309 | export default { 310 | findById, 311 | findByName, 312 | } as const; 313 | 314 | 315 | // UserService.ts 316 | 317 | // PascalCase 318 | import UserRepo from './UserRepo.ts'; 319 | 320 | // Immutable so use UPPER_SNAKE_CASE 321 | const ERRS = { 322 | Foo: 'foo', 323 | Bar: 'bar', 324 | } as const; 325 | 326 | function login() { 327 | ...do stuff 328 | } 329 | 330 | ... 331 | ``` 332 | 333 | ### Types 334 | - Prepend type aliases with a 'T' (i.e. `type TMouseEvent = React.MouseEvent;`) 335 | - Prepend interfaces with an 'I' (i.e. `interface IUser { name: string; email: string; }`) 336 |
337 | 338 | 339 | ## Comments 340 | - Separate files into region as follows (although this could be overkill for files with only one region, use your own discretion): 341 | ```ts 342 | /****************************************************************************** 343 | RegionName (i.e. Constants) 344 | ******************************************************************************/ 345 | ``` 346 | - Separate regions into sections by a `// **** "Section Name" (i.e. Shared Functions) **** //`. 347 | - Use `/** Comment */` above each function declaration ALWAYS. This will help the eyes when scrolling through large files. The first word in the comment should be capitalized and the sentence should end with a period. 348 | - Use `//` for comments inside of functions. The first word in the comment should be capitalized. 349 | - Capitalize the first letter in a comment and use a '.' at the end of complete sentences. 350 | ```typescript 351 | /** 352 | * Function declaration comment. 353 | */ 354 | function foo() { 355 | // Init 356 | const bar = (arg: string) => arg.trim(), 357 | blah = 'etc'; 358 | // Return 359 | return (bar(arg) + bar(arg) + bar(arg)); 360 | } 361 | ``` 362 | - If you need to put comments in an `if else` block put them above the `if` and `else` keywords: 363 | ```typescript 364 | // blah 365 | if (something) { 366 | do_something... 367 | // foo 368 | } else { 369 | do_something else... 370 | } 371 | ``` 372 | 373 | - Don't put spaces within functions generally, but there can be exceptions like between dom elements or hooks elements in React functions. Use `//` comments to separate chunks of logic within functions. Use one space with a `/** */` comment to separate functions. 374 | ```typescript 375 | /** 376 | * Some function 377 | */ 378 | function doThis() { 379 | // Some logic 380 | if (this) { 381 | console.log('dude'); 382 | } 383 | // Some more logic 384 | ...do other stuff blah blah blah 385 | // Return 386 | return retVal; 387 | } 388 | 389 | /** 390 | * Some other function 391 | */ 392 | function doThat() { 393 | // Some other logic 394 | for (const item of arr) { 395 | ...hello 396 | } 397 | // Last comment 398 | if (cool) { return 'yeah'; } 399 | } 400 | ``` 401 | 402 | - If you have a really long function inside of another really long function (i.e. React Hook in a JSX element) you can separate them using `// ** blah ** //`. 403 |
404 | 405 | 406 | ## Imports 407 | - Try to group together similarly related imports (i.e. Service Layer and Repository Layer in an express server). 408 | - Be generous with spacing. 409 | - Put libraries at the top and your code below. 410 | - Try to put code imported from the same folder towards the bottom. 411 | - For imports that extend past the character limit (I use 80), give it a new line above and below but keep it just below the other related imports. 412 | ``` 413 | import express from 'express'; 414 | import insertUrlParams from 'inserturlparams'; 415 | 416 | import UserRepo from '@src/repos/UserRepo'; 417 | import DogRepo from '@src/repos/DogRepo'; 418 | 419 | import { 420 | horseDogCowPigBlah, 421 | horseDogCowPigBlahhhhhh, 422 | horseDogHelllllloooooPigBlahhhhhh, 423 | } from '@src/repos/FarmAnimalRepo'; 424 | 425 | 426 | import helpers from './helpers'; 427 | ``` 428 | 429 | 430 | ## Example Scripts 431 | - Now that we've gone over the main points, let's look at some example scripts. 432 | 433 | - A modular-object script: 434 | ```typescript 435 | // MailUtil.ts 436 | import logger from 'jet-logger'; 437 | import nodemailer, { SendMailOptions, Transporter } from 'nodemailer'; 438 | 439 | 440 | /****************************************************************************** 441 | Constants 442 | ******************************************************************************/ 443 | 444 | const SUPPORT_STAFF_EMAIL = 'do_not_reply@example.com'; 445 | 446 | 447 | /****************************************************************************** 448 | Types 449 | ******************************************************************************/ 450 | 451 | type TTransport = Transporter; 452 | 453 | 454 | /****************************************************************************** 455 | Run (Setup) 456 | ******************************************************************************/ 457 | 458 | const mailer = nodemailer 459 | .createTransport({ ...settings }) 460 | .verify((err, success) => { 461 | if (!!err) { 462 | logger.err(err); 463 | } 464 | }); 465 | 466 | 467 | /****************************************************************************** 468 | Functions 469 | ******************************************************************************/ 470 | 471 | /** 472 | * Send an email anywhere. 473 | */ 474 | function sendMail(to: string, from: string, subject: string, body: string): Promise { 475 | await mailer.send({to, from, subject, body}); 476 | } 477 | 478 | /** 479 | * Send an email to your application's support staff. 480 | */ 481 | function sendSupportStaffEmail(from, subject, body): Promise { 482 | await mailer.send({to: SUPPORT_STAFF_EMAIL, from, subject, body}); 483 | } 484 | 485 | 486 | /****************************************************************************** 487 | Export default 488 | ******************************************************************************/ 489 | 490 | export default { 491 | sendMail, 492 | sendSupportStaffEmail, 493 | } as const; 494 | ``` 495 | 496 | - An inventory script 497 | ```.tsx 498 | // shared-buttons.tsx 499 | 500 | /****************************************************************************** 501 | Components 502 | ******************************************************************************/ 503 | 504 | /** 505 | * Close a html dialog box. 506 | */ 507 | export function CloseBtn() { 508 | return ( 509 | 512 | ); 513 | } 514 | 515 | /** 516 | * Cancel editing a html form. 517 | */ 518 | export function CancelBtn() { 519 | return ( 520 | 523 | ); 524 | } 525 | ``` 526 | 527 | - A declaration script: 528 | ```typescript 529 | // EnvVars.ts 530 | 531 | export default { 532 | port: process.env.PORT, 533 | host: process.env.Host, 534 | databaseUsername: process.env.DB_USERNAME, 535 | ...etc, 536 | } as const; 537 | ``` 538 | 539 | - A linear script: 540 | ```typescript 541 | // server.ts 542 | import express from 'express'; 543 | 544 | 545 | /****************************************************************************** 546 | Run (Setup) 547 | ******************************************************************************/ 548 | 549 | const app = express(); 550 | 551 | app.use(middleware1); 552 | app.use(middleware2); 553 | 554 | doSomething(); 555 | doSomethingElse(); 556 | 557 | 558 | /****************************************************************************** 559 | Export default 560 | ******************************************************************************/ 561 | 562 | export default app; 563 | ``` 564 |
565 | 566 | 567 | ## Misc Style (Don't need to mention things covered by the linter) 568 | - Wrap boolean statements in parenthesis to make them more readable (i.e `(((isFoo && isBar) || isBar) ? retThis : retThat)`) 569 | - Use optional chaining whenever possible. Don't do `foo && foo.bar` do `foo?.bar`. 570 | - Use null coalescing `??` whenever possible. Don't do `(str || '') do `(str ?? '')`. 571 | - For boolean statements, put the variable to the left of the constant, not the other way around: 572 | ``` 573 | // Don't do 574 | if (5 === value) { 575 | 576 | // Do do 577 | if (value === 5) { 578 | ``` 579 | - For Typescript, specify a return type if you are using the function elsewhere in your code. However, always specifying a return type when your function is just getting passed to a library could be overkill (i.e. a router function passed to an express route). Another exception could be JSX function where it's obvious a JSX.Elements is what's getting returned. 580 | - For if statements that are really long, put each boolean statement on it's own line, and put the boolean operator and the end of each statement. For nested boolean statements, use indentation: 581 | ```typescript 582 | if ( 583 | data?.foo.trim() && 584 | data?.bar && ( 585 | role !== ADMIN || 586 | data?.id !== 3 || 587 | name !== '' 588 | ) 589 | ) { 590 | ...doSomething 591 | } 592 | ``` 593 | - When passing object-literals as parameters to function calls, put the first curly brace on the same line as the previous parameter, as following parameters on the same line as the last curly brace: 594 | ```typescript 595 | // Good 596 | fooBar('hello', { 597 | name: 'steve', 598 | age: 13, 599 | }, 'how\'s it going?'); 600 | 601 | // Bad (I see this too much) 602 | fooBar( 603 | 'hello', 604 | { 605 | name: 'steve', 606 | age: 13, 607 | }, 608 | 'how\'s it going?' 609 | ); 610 | 611 | function fooBar(beforeMsg: string, person: IPerson, afterMsg: string): void { 612 | ..do stuff 613 | } 614 | ``` 615 |
616 | 617 | 618 | ## Testing 619 | 620 | ### General Notes 621 | - Anything that changes based on user interaction should be unit-tested. 622 | - All phases of development should include unit-tests. 623 | - Developers should write their own unit-tests. 624 | - Integration tests should test any user interaction that involves talking to the back-end. 625 | - Integration tests may be overkill for startups especially in the early stages. 626 | - Integration tests should be done by a dedicated integration tester who's fluent with the framework in a separate repository. 627 | - Another good reasons for tests are they make code more readable. 628 | - Errors in integration tests should be rare as unit-tests should weed out most of them. 629 | 630 | ### Structuring BDD style tests 631 | - Declare all your variables (except constants) in the `beforeEach`/`beforeAll` callbacks, even ones that don't require asynchronous-initialization. This makes your code cleaner so that everything is declared and initialized in the same place. 632 | - Static constants should go outside of the top level `describe` hook and should go in the `Constants` region like with other scripts. This saves the test runner from having the reinitialize them everytime. 633 | - The tests should should go in a new region between the `Setup` and `Functions` regions. 634 | ```ts 635 | import supertest, { TestAgent } from 'supertest'; 636 | import { IUser } from '@src/modes/User'; 637 | import UserRepo from '@src/repos/UserRepo'; 638 | 639 | 640 | /****************************************************************************** 641 | Constants 642 | ******************************************************************************/ 643 | 644 | const FOO_BAR = 'asdfasdf'; 645 | 646 | 647 | /****************************************************************************** 648 | Tests 649 | ******************************************************************************/ 650 | 651 | describe(() => { 652 | 653 | // Declare here 654 | let testUser: IUser, 655 | apiCaller: TestAgent; 656 | 657 | // Initialize here 658 | beforeEach(async () => { 659 | testUser = User.new(); 660 | await UserRepo.save(testUser); 661 | apiCaller = supertest.agent(); 662 | }); 663 | 664 | describe('Fetch User API', () => { 665 | 666 | it('should return a status of 200 and a user object if the request was ' + 667 | 'successful', async () => { 668 | const resp = await apiCaller.get(`api/users/fetch/${testUser.id}`); 669 | expect(resp.status).toBe(200); 670 | expect(resp.body.user).toEqual(testUser); 671 | }); 672 | 673 | it('should return a status of 404 if the user was not found', async () => { 674 | const resp = await apiCaller.get(`api/users/fetch/${12341234}`); 675 | expect(resp.status).toBe(404); 676 | }) 677 | }); 678 | }); 679 | 680 | 681 | /****************************************************************************** 682 | Functions 683 | ******************************************************************************/ 684 | 685 | /** 686 | * Send an email anywhere. 687 | */ 688 | function _someHelperFn(): void { 689 | ...do stuff 690 | } 691 | ``` 692 | --------------------------------------------------------------------------------