|
| 1 | +#Davstack Service |
| 2 | + |
| 3 | +Davstack Service is simple and flexible library for building backend services with TypeScript. |
| 4 | + |
| 5 | +###Why Use Davstack Service? |
| 6 | + |
| 7 | +- 🏠 Simple and familiar syntax - middleware, input and outputs inspired by trpc procedures |
| 8 | +- 🧩 Flexible - Works well with next js server actions as well as trpc |
| 9 | +- ✅ Typescript first - inferred input/output types and middleware |
| 10 | + |
| 11 | +###Installation |
| 12 | + |
| 13 | +```bash |
| 14 | +npm install zod @davstack/service |
| 15 | +``` |
| 16 | + |
| 17 | +Visit the[DavStack Service Docs](https://davstack.com/service/overview) for more information and examples, such as this[trpc usage example](https://davstack.com/service/trpc-usage-example). |
| 18 | + |
| 19 | +##Demo Usage |
| 20 | + |
| 21 | +- The service definition replaces tRPC procedures, but the syntax is very similar. |
| 22 | +- Once the service is integrated into tRPC routers, the API is the same as any other tRPC router. |
| 23 | + |
| 24 | +##Composing Services example |
| 25 | + |
| 26 | +```ts |
| 27 | +// api/services/invoice.ts |
| 28 | +import {authedService,publicService }from'@/lib/service'; |
| 29 | + |
| 30 | +// Service composed from range of other services: |
| 31 | + |
| 32 | +exportconst mailAiGeneratedInvoice=authedService |
| 33 | +.input(z.object({ to:z.string(), projectId:z.string() })) |
| 34 | +.query(async ({ctx,input })=> { |
| 35 | +awaitcheckSufficientCredits(ctx, { amount:10 }); |
| 36 | + |
| 37 | +const pdf=awaitgeneratePdf(ctx, { html:project.invoiceHtml }); |
| 38 | + |
| 39 | +awaitsendEmail(ctx, { |
| 40 | +to:input.to, |
| 41 | +subject:'Invoice', |
| 42 | +body:'Please find attached your invoice', |
| 43 | +attachments: [{ filename:'invoice.pdf', content:pdf }], |
| 44 | +}); |
| 45 | + |
| 46 | +awaitdeductCredits(ctx, { amount:10 }); |
| 47 | + |
| 48 | +return'Invoice sent'; |
| 49 | +}); |
| 50 | + |
| 51 | +exportconst generatePdf=authedService |
| 52 | +.input(z.object({ html:z.string() })) |
| 53 | +.query(async ({ctx,input })=> { |
| 54 | +// complex business logic here |
| 55 | +returnpdf; |
| 56 | +}); |
| 57 | + |
| 58 | +exportconst sendEmail=authedService |
| 59 | +.input(z.object({ to:z.string(), subject:z.string(), body:z.string() })) |
| 60 | +.query(async ({ctx,input })=> { |
| 61 | +// complex business logic here |
| 62 | +return'Email sent'; |
| 63 | +}); |
| 64 | + |
| 65 | +exportconst checkSufficientCredits=authedService |
| 66 | +.input(z.object({ amount:z.number() })) |
| 67 | +.query(async ({ctx,input })=> { |
| 68 | +// complex business logic here |
| 69 | +return'Sufficient funds'; |
| 70 | +}); |
| 71 | + |
| 72 | +// ... etc |
| 73 | +``` |
| 74 | + |
| 75 | +Integrate your services with tRPC with 0 boilerplate. Works just like any other tRPC router. |
| 76 | + |
| 77 | +```ts |
| 78 | +// api/router.ts |
| 79 | + |
| 80 | +import*asinvoiceServicesfrom'@/api/services/invoice'; |
| 81 | +import {createTRPCRouter }from'@/lib/trpc'; |
| 82 | +import { |
| 83 | +createTrpcProcedureFromService, |
| 84 | +createTrpcRouterFromServices, |
| 85 | +}from'@davstack/service'; |
| 86 | + |
| 87 | +exportconst appRouter=createTRPCRouter({ |
| 88 | +invoice:createTrpcRouterFromServices(invoiceServices), |
| 89 | +}); |
| 90 | +``` |
| 91 | + |
| 92 | +###Middleware Example |
| 93 | + |
| 94 | +Define your services with reusable middleware in a separate file, and export them for reuse. |
| 95 | + |
| 96 | +```ts |
| 97 | +// lib/service.ts |
| 98 | +import {service }from'@davstack/service'; |
| 99 | +import {db }from'@/lib/db'; |
| 100 | + |
| 101 | +// Define the context types for your services |
| 102 | +exporttypePublicServiceCtx= { |
| 103 | +user: { id:string; role:string }|undefined; |
| 104 | +db:typeofdb; |
| 105 | +}; |
| 106 | +exporttypeAuthedServiceCtx=Required<PublicServiceCtx>; |
| 107 | + |
| 108 | +// export your services |
| 109 | +exportconst publicService=service<PublicServiceCtx>(); |
| 110 | + |
| 111 | +exportconst authedService=service<AuthedServiceCtx>().use( |
| 112 | +async ({ctx,next })=> { |
| 113 | +// Only allows authenticate users to access this service |
| 114 | +if (!ctx.user) { |
| 115 | +thrownewError('Unauthorized'); |
| 116 | +} |
| 117 | +returnnext(ctx); |
| 118 | +} |
| 119 | +); |
| 120 | + |
| 121 | +exportfunction createServiceCtx() { |
| 122 | +const user=auth(); |
| 123 | +return {user,db }; |
| 124 | +} |
| 125 | +``` |
| 126 | + |
| 127 | +Import the public / authed service builders from the service |
| 128 | + |
| 129 | +```ts |
| 130 | +// api/services/some-service.ts |
| 131 | +import {publicService,authedService }from'@/lib/service'; |
| 132 | + |
| 133 | +exportconst getSomePublicData=publicService.query(async ({ctx })=> { |
| 134 | +return'Public data'; |
| 135 | +}); |
| 136 | + |
| 137 | +exportconst getSomeUserData=authedService.query(async ({ctx })=> { |
| 138 | +// will throw an error if ctx.user is undefined |
| 139 | +return'Protected data'; |
| 140 | +}); |
| 141 | +``` |
| 142 | + |
| 143 | +Specify the input and output schemas for your service for validation and type safety, and use the ctx/input arguments to access the service context and input data. |
| 144 | + |
| 145 | +```ts |
| 146 | +// api/services/task-services.ts |
| 147 | +import {service }from'@davstack/service'; |
| 148 | +import {z }from'zod'; |
| 149 | + |
| 150 | +const getTasks=service() |
| 151 | +.input(z.object({ projectId:z.string() })) |
| 152 | +.query(async ({ctx,input })=> { |
| 153 | +returnctx.db.tasks.findMany({ where: { projectId:input.projectId } }); |
| 154 | +}); |
| 155 | +``` |
| 156 | + |
| 157 | +###Direct Service Usage |
| 158 | + |
| 159 | +Unlike tRPC procedures, services can be called directly from anywhere in your backend, including within other services. |
| 160 | + |
| 161 | +```typescript |
| 162 | +const ctx=createServiceCtx();// or get ctx from parent service |
| 163 | +const tasks=awaitgetTasks(ctx, { projectId:'...' }); |
| 164 | +``` |
| 165 | + |
| 166 | +This allows you to build complex service logic by composing multiple services together. |
| 167 | + |
| 168 | +```typescript |
| 169 | +const getProjectDetails=service() |
| 170 | +.input(z.object({ projectId:z.string() })) |
| 171 | +.output( |
| 172 | +z.object({ |
| 173 | +id:z.string(), |
| 174 | +name:z.string(), |
| 175 | +tasks:getTasks.outputSchema, |
| 176 | +}) |
| 177 | +) |
| 178 | +.query(async ({ctx,input })=> { |
| 179 | +const project=awaitgetProject(ctx, { projectId:input.projectId }); |
| 180 | +const tasks=awaitgetTasks(ctx, { projectId:input.projectId }); |
| 181 | +return {...project,tasks }; |
| 182 | +}); |
| 183 | +``` |
| 184 | + |
| 185 | +###tRPC Integration |
| 186 | + |
| 187 | +Seamlessly integrate with tRPC to create type-safe API endpoints. |
| 188 | + |
| 189 | +```ts |
| 190 | +import {initTRPC }from'@trpc/server'; |
| 191 | +import {createTrpcRouterFromServices }from'@davstack/service'; |
| 192 | +import*astaskServicesfrom'./services/tasks'; |
| 193 | +import*asprojectServicesfrom'./services/projects'; |
| 194 | +import {sendFeedback }from'./services/send-feedback'; |
| 195 | + |
| 196 | +const t=initTRPC(); |
| 197 | + |
| 198 | +const appRouter=t.router({ |
| 199 | +tasks:createTrpcRouterFromServices(taskServices), |
| 200 | +projects:createTrpcRouterFromServices(projectServices), |
| 201 | +// or create a single procedure from a service |
| 202 | +sendFeedback:createTrpcProcedureFromService(sendFeedback), |
| 203 | +}); |
| 204 | +``` |
| 205 | + |
| 206 | +NOTE: it is recommended to use the`* as yourServicesName` syntax. Otherwise, ctrl+click on the tRPC client handler will navigate you to the app router file, instead of the specific service definition. |
| 207 | + |
| 208 | +###Acknowledgements |
| 209 | + |
| 210 | +Davstack Store has been heavily inspired by[tRPC](https://trpc.io/), a fantastic library for building type-safe APIs. A big shout-out to the tRPC team for their amazing work. |
| 211 | + |
| 212 | +Nick-Lucas, a tRPC contributor, inspired the creation of Davstack Service with his[github comment](https://github.com/trpc/trpc/discussions/4839#discussioncomment-8224476). He suggested "making controllers minimal" and "to separate your business logic from the API logic", which is exactly what Davstack Service aims to do. |
| 213 | + |
| 214 | +###Contributing |
| 215 | + |
| 216 | +Contributions are welcome! Please read our[contributing guide](link-to-contributing-guide) for details on our code of conduct and the submission process. |
| 217 | + |
| 218 | +###License |
| 219 | + |
| 220 | +This project is licensed under the[MIT License](link-to-license). See the LICENSE file for details. |