Skip to content

Commit

Permalink
feat!: major update (desc)
Browse files Browse the repository at this point in the history
Updated deps
`backend` major overhaul:
  + structuring
  + `srvx` for universal serving
  + a damn good providers pattern that supports workerd
Added openApi capability to `backend`
  • Loading branch information
NamesMT committed Dec 18, 2024
1 parent ad5cbb5 commit de40ee2
Show file tree
Hide file tree
Showing 34 changed files with 3,064 additions and 3,237 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,6 @@ yarn-error.log*

# sst
.sst

# pnpm have a bug with tsx that creates the `tsx-0` folder everywhere
tsx-0
20 changes: 11 additions & 9 deletions apps/backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,29 @@

## Structuring cookbook:
#### Root level:
Things like 3rd party APIs, DBs, Storages connectors, etc, should be placed in `~/providers` folder, grouped by their purpose if possible, e.g: `~/providers/auth/kinde-main.ts`, `~/providers/auth/google-main.ts`.
Things like 3rd party APIs, DBs, Storages connectors, etc, should be placed in `~/providers` folder, grouped by their purpose if possible, e.g: `~/providers/auth/kinde-main.ts`, `~/providers/db/neon-main.ts`.

Things that interact with `~/providers` should be placed in `~/services` folder. (like an `user` service)

Other globally reuseable code should be placed in `~/helpers` folder.

Locally reusable code should be placed in the same folder as the file that uses it, its name should be its usable scope, suffixing the file name with `.helper`, e.g: `/api/hello.helper.ts`, `/api/app.helper.ts`.
Locally reusable code should be placed in the same folder as the file that uses it, its name should be its usable scope, suffixing the file name with `.helper`, e.g: `/api/dummy/hello.helper.ts`, `/api/$.helper.ts`.

#### `api` folder:
The main app entry of any folder should be `app.ts`.
The idea of the api folder is to mirrors the actual api url path if possible.

You could create folders to group/prefix the routes, e.g: `/api/auth` folder.
The main app entry of any folder should be `$.ts`, the app entry will not define any routes but to manages which routes are enabled.

Each route should be placed in a separate file according to the route path, e.g: `/api/hello.ts`, `/api/greet.ts`,
Alternatively, you could create a `routes.ts` for multiple routes declaration in file one, e.g: `/api/auth/routes.ts`.
You could create folders to group/prefix the routes, e.g: `/api/auth`, `api/dummy` folder.

If you need to define routes for the current folder path, use `_.ts` for the file name.
Each route should be placed in a separate file according to the route path, e.g: `/api/dummy/hello.ts`, `/api/dummy/greet.ts`,
Alternatively, you could create a `$.routes.ts` for multiple routes declaration in one file, e.g: `/api/auth/$.routes.ts`.

If you need to define routes for the current folder path without any suffix route, use `$$.ts` for the file name, i.e: `/api/$$.ts`

#### Hono `app` export naming conventions:
* If the app contain routes defines, it should be named as: `<Name>RouteApp`, and it should not define the base route path on it's own, it's parent app should define the base route path for it, e.g: `/api/app.ts` `.route` other `RouteApp`s and define the base path for them.
* For other cases (i.e: main `app.ts` entry), it should be named as: `<Name>App`, and it should only `.route` other instances or `.use` middlewares, do not define routes on the `App` instance.
* For main app entries ($.ts), it should be named as: `<Name>App`, and it should only `.route` other instances or `.use` middlewares, do not define routes on the `App` instance.
* If the app contain routes defines, it should be named as: `<Name>RouteApp`

#### `import` ordering:
Imports should not be separated by empty lines, and should be sorted automatically by eslint.
16 changes: 10 additions & 6 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,25 @@
"check": "pnpm lint && pnpm test:types && vitest run --coverage",
"build": "dotenvx run -f .env.prod.local -f .env -- unbuild"
},
"dependencies": {
"srvx": "^0.1.3"
},
"devDependencies": {
"@hono/arktype-validator": "^2.0.0",
"@hono/node-server": "^1.13.7",
"@kinde-oss/kinde-typescript-sdk": "^2.9.1",
"@local/common": "workspace:*",
"@local/tsconfig": "workspace:*",
"@namesmt/utils": "^0.5.9",
"@vitest/coverage-v8": "^2.1.5",
"arktype": "2.0.0-rc.14",
"@scalar/hono-api-reference": "^0.5.165",
"@vitest/coverage-v8": "^2.1.8",
"arktype": "2.0.0-rc.25",
"consola": "^3.2.3",
"hono": "^4.6.10",
"hono": "^4.6.14",
"hono-adapter-aws-lambda": "^1.3.0",
"hono-openapi": "^0.2.1",
"hono-sessions": "^0.7.0",
"std-env": "^3.8.0",
"unbuild": "^2.0.0",
"vitest": "^2.1.5"
"unbuild": "^3.0.1",
"vitest": "^2.1.8"
}
}
4 changes: 4 additions & 0 deletions apps/backend/src/api/$$.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { appFactory } from '~/helpers/factory'

export const apiRouteApp = appFactory.createApp()
.get('', async c => c.text('OK'))
21 changes: 11 additions & 10 deletions apps/backend/src/api/app.ts → apps/backend/src/api/$.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
import { appFactory } from '~/factory'
import { authApp } from './auth/app'
import { greetRouteApp } from './greet'
import { helloRouteApp } from './hello'
import { appFactory } from '~/helpers/factory'
import { apiRouteApp } from './$$'
import { authApp } from './auth/$'
import { dummyApp } from './dummy/$'

export const apiApp = appFactory.createApp()
// Simple health check route
.route('', apiRouteApp)

// Auth app - you'll need to setup Kinde environment variables.
.route('/auth', authApp)

// Simple health check route
.route('/hello', helloRouteApp)

// Simple greet route for arktype input validation demo
.route('/greet', greetRouteApp)
// A dummy app for some demos
.route('/dummy', dummyApp)

// ### This block contains the sample code for streaming APIs,
// import type { TypedResponse } from 'hono'
// import { streamText } from 'hono/streaming'

// Do note that SST doesn't support Live Development for Lambda streaming API yet: https://github.com/sst/ion/issues/63
// Do note that SST doesn't support Live Development for Lambda streaming API yet: https://sst.dev/docs/component/aws/function/#streaming

// For RPC to know the type of streamed endpoints you could manually cast it with TypedResponse 👌
// .get('/helloStream', c => streamText(c, async (stream) => {
// await stream.writeln('Hello from Hono `/api/helloStream`!')
// }) as Response & TypedResponse<'Hello from Hono `/api/helloStream`!'>)
// ###
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import type { ClaimTokenType, FlagType } from '@kinde-oss/kinde-typescript-sdk'
import { env } from 'std-env'
import { appFactory } from '~/factory'
import { appFactory } from '~/helpers/factory'
import { getSessionManager } from '~/helpers/kinde'
import { kindeClient } from '~/providers/auth/kinde-main'
import { getKindeClient } from '~/providers/auth/kinde-main'

export const authRoutesApp = appFactory.createApp()
.get('/health', async (c) => {
return c.text('Good', 200)
})

.get('/login', async (c) => {
const kindeClient = getKindeClient()
const org_code = c.req.query('org_code')

const loginUrl = await kindeClient.login(getSessionManager(c), { org_code })

c.get('session').set('backToPath', c.req.query('path'))
Expand All @@ -19,12 +21,17 @@ export const authRoutesApp = appFactory.createApp()
})

.get('/register', async (c) => {
const kindeClient = getKindeClient()
const org_code = c.req.query('org_code')

const registerUrl = await kindeClient.register(getSessionManager(c), { org_code })

return c.redirect(registerUrl.toString())
})

.get('/callback', async (c) => {
const kindeClient = getKindeClient()

await kindeClient.handleRedirectToApp(getSessionManager(c), new URL(c.req.url))

let backToPath = c.get('session').get('backToPath') as string || '/'
Expand All @@ -35,49 +42,75 @@ export const authRoutesApp = appFactory.createApp()
})

.get('/logout', async (c) => {
const kindeClient = getKindeClient()

const logoutUrl = await kindeClient.logout(getSessionManager(c))

return c.redirect(logoutUrl.toString())
})

.get('/isAuth', async (c) => {
const kindeClient = getKindeClient()

const isAuthenticated = await kindeClient.isAuthenticated(getSessionManager(c)) // Boolean: true or false

return c.json(isAuthenticated)
})

.get('/profile', async (c) => {
const kindeClient = getKindeClient()

const profile = await kindeClient.getUserProfile(getSessionManager(c))

return c.json(profile)
})

.get('/createOrg', async (c) => {
const kindeClient = getKindeClient()
const org_name = c.req.query('org_name')?.toString()

const createUrl = await kindeClient.createOrg(getSessionManager(c), { org_name })

return c.redirect(createUrl.toString())
})

.get('/getOrg', async (c) => {
const kindeClient = getKindeClient()

const org = await kindeClient.getOrganization(getSessionManager(c))

return c.json(org)
})

.get('/getOrgs', async (c) => {
const kindeClient = getKindeClient()

const orgs = await kindeClient.getUserOrganizations(getSessionManager(c))

return c.json(orgs)
})

.get('/getPerm/:perm', async (c) => {
const kindeClient = getKindeClient()

const perm = await kindeClient.getPermission(getSessionManager(c), c.req.param('perm'))

return c.json(perm)
})

.get('/getPerms', async (c) => {
const kindeClient = getKindeClient()

const perms = await kindeClient.getPermissions(getSessionManager(c))

return c.json(perms)
})

// Try: /api/auth/getClaim/aud, /api/auth/getClaim/email/id_token
.get('/getClaim/:claim', async (c) => {
const kindeClient = getKindeClient()
const type = (c.req.query('type') ?? 'access_token') as ClaimTokenType

if (!/^(?:access_token|id_token)$/.test(type))
return c.text('Bad request: type', 400)

Expand All @@ -86,16 +119,22 @@ export const authRoutesApp = appFactory.createApp()
})

.get('/getFlag/:code', async (c) => {
const kindeClient = getKindeClient()

const claim = await kindeClient.getFlag(
getSessionManager(c),
c.req.param('code'),
c.req.query('default'),
c.req.query('flagType') as keyof FlagType | undefined,
)

return c.json(claim)
})

.get('/getToken', async (c) => {
const kindeClient = getKindeClient()

const accessToken = await kindeClient.getToken(getSessionManager(c))

return c.text(accessToken)
})
5 changes: 5 additions & 0 deletions apps/backend/src/api/auth/$.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { appFactory } from '~/helpers/factory'
import { authRoutesApp } from './$.routes'

export const authApp = appFactory.createApp()
.route('', authRoutesApp)
5 changes: 0 additions & 5 deletions apps/backend/src/api/auth/app.ts

This file was deleted.

8 changes: 8 additions & 0 deletions apps/backend/src/api/dummy/$.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { appFactory } from '~/helpers/factory'
import { dummyGreetRouteApp } from './greet'
import { dummyHelloRouteApp } from './hello'

export const dummyApp = appFactory.createApp()
.route('/hello', dummyHelloRouteApp)

.route('/greet', dummyGreetRouteApp)
30 changes: 30 additions & 0 deletions apps/backend/src/api/dummy/greet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { type } from 'arktype'
import { describeRoute } from 'hono-openapi'
import { resolver } from 'hono-openapi/arktype'
import { customArktypeValidator } from '~/helpers/arktype'
import { appFactory } from '~/helpers/factory'

export const dummyGreetRouteApp = appFactory.createApp()
.get(
'',
describeRoute({
description: 'Say hello to a user',
responses: {
200: {
description: 'Successful response',
content: {
'text/plain': { schema: resolver(
type('string'),
) },
},
},
},
}),
customArktypeValidator('query', type({
name: 'string>0',
})),
async (c) => {
const { name } = c.req.valid('query')
return c.text(`Hello ${name}!`)
},
)
File renamed without changes.
5 changes: 5 additions & 0 deletions apps/backend/src/api/dummy/hello.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { appFactory } from '~/helpers/factory'
import { getHelloMessage } from './hello.helper'

export const dummyHelloRouteApp = appFactory.createApp()
.get('', async c => c.text(getHelloMessage()))
15 changes: 0 additions & 15 deletions apps/backend/src/api/greet.ts

This file was deleted.

5 changes: 0 additions & 5 deletions apps/backend/src/api/hello.ts

This file was deleted.

37 changes: 37 additions & 0 deletions apps/backend/src/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { cors } from 'hono/cors'
import { logger as loggerMiddleware } from 'hono/logger'
import { env } from 'std-env'
import { errorHandler } from '~/helpers/error'
import { appFactory, triggerFactory } from '~/helpers/factory'
import { cookieSession } from '~/middlewares/session'
import { apiApp } from './api/$'
import { logger } from './helpers/logger'
import { providersInit } from './providers'

export const app = appFactory.createApp()
// Initialize providers
.use(providersInit)

// Register global not found handler
.notFound(c => c.text('four-o-four', 404))

// Register global error handler
.onError(errorHandler)

// Request logging middleware
.use(loggerMiddleware(logger.log))

// Register trigger routes, after the logging middleware but before the request-based middlewares
.route('/', triggerFactory.honoApp)

// CORS middleware
.use(cors({
origin: [env.FRONTEND_URL!],
credentials: true,
}))

// Session management middleware, configure and see all available managers in `src/middlewares/session.ts`
.use(await cookieSession())

// Register API routes
.route('/api', apiApp)
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { arktypeValidator } from '@hono/arktype-validator'
import { DetailedError } from '@namesmt/utils'
import { validator as arktypeValidator } from 'hono-openapi/arktype'

type arktypeValidatorArguments = Parameters<typeof arktypeValidator>
export function customArktypeValidator<Target extends arktypeValidatorArguments[0], Schema extends arktypeValidatorArguments[1]>(target: Target, schema: Schema) {
Expand Down
4 changes: 2 additions & 2 deletions apps/backend/src/helpers/error.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { ErrorHandler } from 'hono'
import type { StatusCode } from 'hono/utils/http-status'
import type { HonoEnv } from '~/types'
import { DetailedError } from '@namesmt/utils'
import { HTTPException } from 'hono/http-exception'
import { logger } from '~/logger'
import type { HonoEnv } from '~/types'
import { logger } from '~/helpers/logger'

export const errorHandler: ErrorHandler<HonoEnv> = (err, c) => {
logger.error(err)
Expand Down
Loading

0 comments on commit de40ee2

Please sign in to comment.