Skip to content

Instantly share code, notes, and snippets.

@jonerer
Last active January 14, 2023 18:28
Show Gist options
  • Save jonerer/52493a72b8cf48359789c5f60595061f to your computer and use it in GitHub Desktop.
Save jonerer/52493a72b8cf48359789c5f60595061f to your computer and use it in GitHub Desktop.
API Constructor
Concept for generating boilerplate for API
===
One issue is that the input and output parts are TS, and the others could be done with a DSL. (in order to not have to re-invent zod basically).
I used typescript interfaces for DTOs and zod for input, and came up with something like this:
For the DSL it could look like so:
===
types ts```{
CommentUpdateBody = z.object({
text: z.string()
})
interface CommentDto {
id: number
text: string
updates: number
}
}```
route "/comments" {
// "model" here is used to tell the endpoint what to populate the context with for ":id"
model comment
// response dto will be pluralized for index paths
// response should be serialized into dto automatically (shaving off unwanted fields etc)
response CommentDto
get "/"
post "/:id" {
// default name "update"
body CommentUpdateBody
}
}
Perhaps "model" could be skipped, if the endpoint path is something like "/:commentId"
===
For the "update" endpoint this should generate a file with some boilerplate and some types.
Types like
interface EndpointCommentsContext {
params: {
id: string
comment: prisma.Comment
}
body: z.infer<typeof CommentUpdateBody>
req: Request
res: Response
}
export type EndpointCommentsUpdate = (
ctx: EndpointCommentsContext
) => Promise<CommentDto>
That can be used like
export const update: EndpointCommentsUpdate = async (ctx) => {
const updated = await prisma.comment.update({
data: {
updates: ctx.params.comment.updates + 1,
text: ctx.body.text,
},
where: {
id: ctx.params.comment.id,
},
})
return updated
}
===
This could be supported by some generated boilerplate like
export function toCommentDto(retval: CommentDto): CommentDto {
return {
id: +retval.id,
text: "" + retval.text,
updates: +retval.updates,
}
}
export const Register = (router: Router, prisma: prisma.PrismaClient) => {
router.post("/comments/:id", async function (req, res) {
try {
// param
const id = req.params.id
const comment = await prisma.comment.findFirst({
where: {
id: +id,
},
})
if (!comment) {
res.status(404).send("404 comment not found")
return
}
const zodThing = CommentUpdateBody.safeParse(req.body)
if (!zodThing.success) {
res.status(400).send("Invalid request body")
return
}
const ctx: EndpointCommentsContext = {
params: {
comment,
id,
},
body: zodThing.data,
req,
res,
}
const retval = await CommentsUpdate(ctx)
const dtod = toCommentDto(retval)
res.status(200).send(dtod)
} catch (e) {
res.status(500).send("Internal server error")
return
}
})
}
===
I also wrote a parser for this in "Peggy" https://peggyjs.org/online.html
start
= Statements
Statements
= Statement*
Statement =
TypesBlock
/ RouteBlock
/ CommentLine
/ __
RouteBlock
= "route" __ "\"" path:[/a-z]+ "\"" _ "{" _
stmts:RouteStatements*
_ "}" { return `{type: "route", path: "${path.join('')}"}, statements: [${stmts.join(",")}"` }
RouteStatements
= RouteStatement+
RouteStatement
= ModelStatement
/ ResponseStatement
/ EndpointBlock
/ EndpointShorthand
/ __
EndpointShorthand
= verb:("get" / "post") __ "\"" path:[:/a-z]+ "\""
{ return `{ type: "endpoint", verb: "${verb}", path: "${path.join("")}", body: [] }` }
EndpointBlock
= verb:("get" / "post") __ "\"" path:[:/a-z]+ "\"" _ "{" _
body:EndpointBlockStatements
_ "}" { return `{ type: "endpoint", verb: "${verb}", path: "${path.join("")}", body: [${body}] }` }
EndpointBlockStatements = EndpointBlockStatement+
EndpointBlockStatement
= BodyStatement
/ CommentLine
/ __
BodyStatement
= "body" __ name:[a-zA-Z]+
{ return `{ type: "body", name: ${name.join("")} }` } // <--- jobbar här!
ModelStatement
= "model" __ name:[a-zA-Z]+
{ return `{type: "model", name: "${name.join('')}" }` }
ResponseStatement
= "response" __ name:[a-zA-Z]+
{ return `{type: "response", name: "${name.join('')}" }` }
// https://stackoverflow.com/questions/39604207/peg-js-get-any-text-between-and
TypesBlock "Types block"
= "types" _ "ts```" _ body:TextUntilTerminator _ "```" { return body.join(""); }
// { return "minbody:" + body }
TextUntilTerminator
= x:(&HaveTerminatorAhead .)* { return x.map(y => y[1]) }
HaveTerminatorAhead
= . (!"```" .)* "```"
CommentLine
= [ \t]* "//" [^\n]* "\n"
{}
__ "whitespace"
= [ \t\n\r]+
{return ''}
_ "maybe whitespace"
= [ \t\n\r]*
{return ''}
@jonerer
Copy link
Author

jonerer commented Jan 13, 2023

This is assuming a prisma model like

model Comment {
id Int @id
updates Int
originalText String
text String
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment