Skip to content

Instantly share code, notes, and snippets.

@abhisekpadhi
Created August 3, 2023 20:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save abhisekpadhi/26589c697a5ea193f02318e1bd912122 to your computer and use it in GitHub Desktop.
Save abhisekpadhi/26589c697a5ea193f02318e1bd912122 to your computer and use it in GitHub Desktop.
Engineering philosophy

Why monorepo?

  • 🚀 Team can move fast: reduced context switching
  • ♥️ Developer experience: Code sharing, consistent tooling

General coding sanity:

  • Do not prematurely optimise. Do not create new file for a function that is not going to be shared by more than 1 file, only to clean up cluttered code. Keep related things together. Don't prematurely break a file into multiple files just because it looks cluttered. The best reason to declutter should be few functions are shared across multiple files. The file is very big 5K-10K lines of code, opening and closing takes time because it may clog up the buffer.

  • Do not group functionality based on the tech requirement. Group them based on business needs. In short term it will seem cool and clean but in longer term it becomes a bottleneck with a large team. The end goal of code organisation is that - when a new engineer joins, he/she must be able to grasp the business functionality just by plain reading the code.

Pull requests

  • PR's are supposed to be small, quickly digestable, result in fast review cycles.
  • You must only include those files in the PR that are relevant to that PR. Please don't include files with indentation changes or cosmetic changes.
  • Partial/incomplete/broken PR (pull requests) are very much welcome, as long as the product is not live.
  • In future also, broken and incomplete PR are welcome as long as it will not disrupt the system if deployed.
  • As a small tech startup our strength is shipping features fast, of course it will sometimes break things, but only if it's pre-estimated and contingency is in place to mitigate any risk of loosing business.

API:

  • req flow: router -> workflows -> repository
    • workflows: user journeys, biz logic
    • routers: trpc routers
    • repositories: db calls
    • lib: shared functionalities
      • Web:-
        • lib: shared code
        • page: nextjs page routes
        • workflows: user journeys
      • Opinionated:
        • No ORM (like prisma): we want total control on how backend talks with db. we want control on the optimisations.
        • Sql attacks: stop them at the edge i.e router. use zod schema for input, sanitise at workflows level before writing to db.
        • Any user generated inputs (alien) should be passed through zod schema parser for data sanity.
        • Use LOG instead of console.log for logging for production.
        • Use CONSTANTS for arbitrary values, key-values, configs.

Models:

  • UserAccountSchema is an representation of table userAccount in db.

    • TUserAccount is a type inferred from the schema.
    • UserAccount is a class used for transferring data between to and from DB calls, so we don’t guess the type of data & shape of data.
    • BaseDTO roughly translates to base data transfer object. Like I said the classes that extend this base
    • class are used for data transferring between different functions.
    • For API requests, responses - derived schemas are used such as UserAccountUpdateReqSchema.
    • TUserAccount is a type inferred from the schema.
    • UserAccount is a class used for transferring data between to and from DB calls, so we don’t guess the type of data & shape of data.
    • BaseDTO roughly translates to base data transfer object. Like I said the classes that extend this base
    • class are used for data transferring between different functions.
    • For API requests, responses - derived schemas are used such as UserAccountUpdateReqSchema.
  • Derived schemas contains subset of properties of the main schema.

  • TSomething is a way of inferring a typescript type out of a zod schema SomethingSchema .

    • Follow the naming convention:
    • If it’s a schema, suffix the name with Schema
    • If it’s a type, prefix the name with T
    • Prefer types over interfaces. We follow immutability approach in development.
  • yes, one more thing AuditableSchema signifies a schema/db table is auditable.

  • ⭐ Whats auditable?

    • So, we treat all our db table rows as immutable data. We will never overwrite them using UPDATE sql queries.
    • Rather we use a column called currentActive to mark which is the latest row for a particular unique data. createdAt gives us what time the row was inserted.
    • For example - userAccount table as a column called profilePicUrl , lets say you need to update it for a user.
    • You mark the old row for that user as currentActive = 0 and then insert a new row with updated data.
    • if you see the file userAccountRepository.tsx
      • You will see we are deactivating old row for that user and inserting a new row in a transaction of course.
    • Read more technical recipies

Function return types

  • Don't let next developer guess whats the return type of a function your wrote.
  • Define a return type for your function and put it in the function signature.
  • For example:
type TUserP = {
	foo: string;
};

type TAdminP = {
	bar: string;
};

// return type for your function
type TPerm = {
	role: string;
	data: string;
};

// I don't need to understand what the function does to figure out what is the return type
async function getPermission(role: 'user' | 'admin'): Promise<TPerm> {
	let data = '';
	// ... your logic ...
	return { role, data } as TPerm;
}

zut-web folder structure

  • Top level structure: top level folders
    • No business logic to be present in pages/xyz.tsx, use this only for routing.
    • Ex: pages/products.tsx will be made up of components from workflows/products/ProductsScreen.tsx (holds business logic).
  • Workflows folder internal structure: workflows folder interal
    • Component specific css should be present near the component
  • State management practices:
    • The logic that modifies state should stay close to where the state is defined
    • Use (action -> reducers -> state) model of managing state, it's easier to read and debug at scale
    • Model store's around business workflows/entities, not tech requirements
    • Do not directly manipulate state

UI Components

Hygiene you should maintain when designing components:

  • Figure out if the component is going to be common/shared component, that can be reused. Ex: buttons, tabs, tooltips, inputs etc.

  • create a components whose name starts with Zui (ex: ZuiButton ) and put it in zut-web/ui-components

  • use that components in the zut-web project or any other frontend project

  • ZuiButton can internally contain mui button component

    • Why:
      • any day in future we can swap between ui library or do our own custom implementation
      • if there is a bug or customisation necessary, we can fix/control from one single point
      • components can be reused => less code to write overally, predictable user experiences.

Note about dealing with files in S3:

  • Generally speaking, every time there is S3 involved in storing file either temporarily or permanently, we keep the relative path in the db in the relevant field the supposed to contain the url to that file. It could be image, excel etc.
  • We never store the absolute url.
  • Absolute url is computed by concatening the cloudfront url base + the path obtained from db.
  • The cloudfront url base points to the s3 bucket.
  • This design helps us to change bucket name in future without touch code or any data in db. Not that we will need it, but it prepares us for disaster.
  • This means everytime a url is required to be present in the response sent from the server, the server is required to build the absolute url and send it to frontend. FE will not supposed to know how to construct absolute url of a file in s3. Example: the S3 bucket name might be called zut-user-uploads and the url we store db could be /product/prd_1234asdasd/media/med_123123123.png . When server spits out api response, the url will be modified to cdn.zut.ai/product/prd_1234asdasd/media/med_123123123.png .

File upload to s3 (checklist)

  • Generate presigned url for PutObject command with s3 client on server side, create a empty media entity and save to db.
  • Frontend will use presigned url to upload the file & call backend to inform after upload is completed for any further processing.
  • Build a cron job that will take care of deleting temporary files and unused files by going through media table.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment