A guide to patterns and conventions for writing consistent code
Type
Convention
Example
Page
Next.js reserved names
page.tsx, layout.tsx
Component
kebab-case
jam-now.tsx, bottom-sheet.tsx
Compound component
.component.tsx suffix
member.component.tsx
Server Action
verb-based
create.ts, read.ts, delete.ts
Enum
.enum.ts suffix
genre.enum.ts, position.enum.ts
Interface
.interface.ts suffix
entity.interface.ts
Utility
verb-based
validate.ts, convert.ts
Type
Convention
Example
Function
camelCase, starts with a verb
createJamEntry, fetchJams
Variable
camelCase
isLoading, userProfile
Constant
UPPER_SNAKE_CASE
MAX_HEADCOUNT
Interface
PascalCase, prefixed
Entity_User, CreateJamDto
Enum
PascalCase
GenreEnum, PositionEnum
Context
camelCase
globalContext
Prefix
Purpose
Example
Entity_
DB entity
Entity_User, Entity_JamSession
Create
Creation DTO
CreateJamDto, CreateJamEntryDto
ZodType_
Zod inferred type
ZodType_Song, ZodType_HeadCnt
Filter_
Filter parameters
Filter_JamSessionFeed
// app/(pages)/(only-user)/jamsession/page.tsx
"use client" ;
// 1. Imports (standard library β external β internal)
import { useContext , useEffect , useRef , useState } from "react" ;
import moment from "moment" ;
import { LocationEnum } from "@/app/lib/enums/location.enum" ;
import { globalContext } from "../../context" ;
import TopNavBar from "@/app/ui/navbar/jamsession/top" ;
export default function JamSession ( ) {
// 2. Context
const { bottomSheet, session } = useContext ( globalContext ) ;
// 3. Refs
const childRef = useRef < FeedMethods > ( null ) ;
// 4. States
const [ filter , setFilter ] = useState < Filter > ( { /* ... */ } ) ;
const [ isLoading , setIsLoading ] = useState ( true ) ;
// 5. Effects
useEffect ( ( ) => {
// Initialization logic
} , [ dependencies ] ) ;
// 6. Handlers (if needed)
const handleClick = ( ) => { /* ... */ } ;
// 7. Render
return (
< div className = "..." >
< TopNavBar / >
{ /* ... */ }
< / d i v >
) ;
}
// app/ui/buttons/jam-now.tsx
"use client" ;
import { Dispatch , SetStateAction , useState } from "react" ;
// Define Props as an inline type
export default function JamNow ( {
selectedDate,
setSelectedDate,
refetch,
} : {
selectedDate : moment . Moment ;
setSelectedDate : Dispatch < SetStateAction < moment . Moment > > ;
refetch : ( ) => void ;
} ) {
// Local state
const [ isTurnedOn , setIsTurnedOn ] = useState ( false ) ;
return (
< span onClick = { ( ) => setIsTurnedOn ( true ) } >
{ /* ... */ }
< / s p a n >
) ;
}
Compound Component (Render Function)
When many props are required, write it as a function:
// app/(pages)/(only-user)/myact/[id]/components/member.component.tsx
"use client" ;
export const MemberComponent = (
idx : number ,
jam : Entity_JamSession ,
entry : Entity_JamEntry ,
isMyJam : boolean ,
letOutMember : ( entryId : number ) => void ,
bottomSheet : BottomSheetContext ,
) => {
const { nickname, profile_image } = entry . user ;
const isPast = moment ( jam . created_at ) . isBefore ( moment ( ) ) ;
return (
< div key = { idx } className = "..." >
{ /* ... */ }
< / d i v >
) ;
} ;
// Usage
{ entries . map ( ( entry , idx ) => MemberComponent ( idx , jam , entry , isMyJam , letOutMember , bottomSheet ) ) }
// app/api/functions/create.ts
"use server" ;
import { CustomError } from "@/app/lib/errors/custom-error" ;
import { createClient } from "@/app/util/supabase/server" ;
import { CreateJamEntryDto } from "../dtos/create" ;
import { sanitize } from "@/app/lib/functions/validate" ;
export async function createJamEntry (
input : CreateJamEntryDto
) : Promise < boolean > {
let ret : boolean = false ;
try {
// 1. Validate/sanitize input
input . appeal = await sanitize ( input . appeal ) ;
// 2. Create Supabase client
const supabase = await createClient ( ) ;
// 3. DB operation
const { error } = await supabase . from ( "jamentry" ) . insert ( input ) ;
// 4. Handle result
ret = ! error ;
} catch ( e ) {
// 5. Error handling
if ( process . env . NODE_ENV === "development" ) console . error ( e ) ;
if ( e instanceof CustomError ) throw e ;
else throw CustomError . UnExpected ;
}
return ret ;
}
Read Pattern (Relation Query)
// app/api/functions/read.ts
"use server" ;
export async function getJam ( id : number ) : Promise < Entity_JamSession > {
let ret : Entity_JamSession = null ;
try {
const supabase = await createClient ( ) ;
const user = await getUser ( supabase ) ;
// Relation query
const { data, error } = await supabase
. from ( "jamsession" )
. select ( `
*,
user ( nickname, pos, genre, level, bpm, profile_image ),
jamentry ( id, jam_id, user_id, applied_pos )
` )
. eq ( "id" , id )
. single ( ) ;
if ( error ) throw CustomError . UnExpected ;
if ( ! data ) throw CustomError . InvalidJam ;
// Add computed fields
data . curHeadCnt = data . jamentry . length ;
data . maxHeadCnt = data . headcnt_by_pos . reduce (
( acc , cur ) => acc + cur . headcnt , 0
) ;
ret = data ;
} catch ( e ) {
if ( e instanceof CustomError ) throw e ;
else throw CustomError . UnExpected ;
}
return ret ;
}
API Route Handler Pattern
// app/api/maniadb/route.ts
import axios from "axios" ;
import { NextRequest , NextResponse } from "next/server" ;
import { FetchResult } from "../types/response.interface" ;
export async function POST ( req : NextRequest ) {
// 1. Parse request
const { songName } = await req . json ( ) ;
// 2. Validate input
if ( ! songName ) {
return NextResponse . json ( {
status : 500 ,
msg : "Failed to get the name of song." ,
data : null ,
} ) ;
}
try {
// 3. Call external API
const data : FetchResult = await axios
. get ( `https://www.maniadb.com/api/search/${ songName } /?...` )
. then ( ( res ) => ( {
status : 200 ,
msg : "Success" ,
data : res . data ,
} ) )
. catch ( ( ) => ( {
status : 500 ,
msg : "Failed to request" ,
data : null ,
} ) ) ;
// 4. Return response
return NextResponse . json ( data ) ;
} catch ( e ) {
return NextResponse . json ( {
status : 500 ,
msg : "Error" ,
data : null ,
} ) ;
}
}
// app/lib/errors/custom-error.ts
const postfix = " λ¬Έμ κ° μ§μμ μΌλ‘ λ°μνλ€λ©΄ νΌλλ°±μ μ μ‘ν΄μ£ΌμΈμ." ;
export class CustomError extends Error {
constructor ( msg : string ) {
super ( msg ) ;
this . name = "CustomError" ;
}
// Common errors
static Forbidden = new CustomError ( "κΆνμ΄ μμ΅λλ€." ) ;
static UnExpected = new CustomError ( "μκΈ°μΉ λͺ»ν λ¬Έμ κ° λ°μνμ΅λλ€." + postfix ) ;
static InvalidUserSession = new CustomError ( "νμμ 보λ₯Ό λΆλ¬μ€λ λ°μ μ€ν¨νμ΅λλ€." + postfix ) ;
static InvalidJam = new CustomError ( "μ ν¨νμ§ μμ μΌμΈμ
μ
λλ€." ) ;
static InvalidUser = new CustomError ( "μΌμΉνλ μ μ λ₯Ό μ°Ύμ μ μμ΅λλ€." ) ;
// Server-only
static UpdatePlaylistItem = new CustomError ( "μ°μ£Όλͺ©λ‘ νλͺ© μ
λ°μ΄νΈμ μ€ν¨νμ΅λλ€." + postfix ) ;
static UploadImage = new CustomError ( "μ΄λ―Έμ§ μ
λ‘λμ μ€ν¨νμ΅λλ€." + postfix ) ;
// Client-only
static CreateJamSession = new CustomError ( "μΌμΈμ
μ μμ±νλλ°μ μ€ν¨νμ΅λλ€." + postfix ) ;
static SearchSong = new CustomError ( "곑 κ²μμ μ€ν¨νμ΅λλ€." + postfix ) ;
}
try {
// Business logic
const { data, error } = await supabase . from ( "..." ) . select ( ) ;
if ( error ) throw CustomError . UnExpected ;
if ( ! data ) throw CustomError . InvalidJam ;
// Handle success
return data ;
} catch ( e ) {
// Log only in the development environment
if ( process . env . NODE_ENV === "development" ) console . error ( e ) ;
// Rethrow CustomError as-is
if ( e instanceof CustomError ) throw e ;
// Wrap unknown errors as UnExpected
else throw CustomError . UnExpected ;
}
// Include FK-related data
const { data } = await supabase
. from ( "jamsession" )
. select ( `
*,
user ( nickname, pos, genre, level, profile_image ),
jamentry ( id, user_id, applied_pos, is_allowed )
` )
. eq ( "id" , id )
. single ( ) ;
Filter Query (Conditional)
let query = supabase . from ( "jamsession" ) . select ( `*` ) ;
// Add conditional filters
if ( filter . location ) {
query = query . contains ( "location" , [ MapLocationToData [ filter . location ] ] ) ;
}
if ( filter . genre ) {
query = query . contains ( "genre" , [ MapGenreToData [ filter . genre ] ] ) ;
}
if ( filter . level ) {
query = query . contains ( "level" , [ MapLevelToData [ filter . level ] ] ) ;
}
// Sorting and pagination
query = query
. order ( "datetime" , { ascending : true } )
. range ( pageParam * 6 , pageParam * 6 + 5 ) ;
const { data } = await query ;
// Upload image
const { data, error } = await supabase . storage
. from ( process . env . SUPABASE_STORAGE_NAME ! )
. upload ( `jamsession/${ sessionId } /${ filename } ` , file ) ;
// Generate public URL
const { data : { publicUrl } } = supabase . storage
. from ( process . env . SUPABASE_STORAGE_NAME ! )
. getPublicUrl ( data . path ) ;
State Management Patterns
// Retrieve
const { bottomSheet, session, setSmallLoading } = useContext ( globalContext ) ;
// Open Bottom Sheet
bottomSheet . set ( {
children : < MyComponent onClose = { ( ) => bottomSheet . set ( { children : null } ) } / > ,
location : BottomSheetHeight . Half ,
} ) ;
// Update user profile
session . setUserProfile ( { nickname : "μλλ€μ" } ) ;
// Toggle loading
setSmallLoading ( ) ;
React Query Infinite Scroll
const { data, fetchNextPage, hasNextPage, isLoading, refetch } = useInfiniteQuery ( {
queryKey : [ "jamsessions" , filter ] ,
queryFn : ( { pageParam = 0 } ) => fetchJams ( { pageParam, filter } ) ,
getNextPageParam : ( lastPage ) => lastPage . nextPage ?? undefined ,
initialPageParam : 0 ,
staleTime : 1000 * 60 * 5 , // 5-minute caching
refetchOnWindowFocus : false ,
} ) ;
// Intersection Observer integration
const { ref, inView } = useInView ( ) ;
useEffect ( ( ) => {
if ( inView && hasNextPage && ! isLoading ) {
fetchNextPage ( ) ;
}
} , [ inView , hasNextPage , isLoading ] ) ;
// Rendering
return (
< >
{ data ?. pages . map ( ( page ) =>
page . data . map ( ( item ) => < Card key = { item . id } { ...item } / > )
) }
< div ref = { ref} / > { /* Trigger element */ }
< / >
) ;