There are tons of questions about Services. And rightfully so. They’re Redwood’s most interesting abstraction. Interest only magnified after @mojombo talked about them on Frontend First, and it hasn’t waned since.
While we still don’t have all the answers, we thought we could at least fill you in on what we know right now. So here I’ll try to summarize everything I know about Services to date. Take everything with a grain of salt. This isn’t a spec, it’s just an attempt to explain.
Let’s start with a topic that I think will clear up most of the confusion around Services:
Services haven’t been fully introduced
If you’ve been poking around the graphql.js
Function that comes with Redwood apps, you might’ve noticed that there’s this function called makeServices
there:
// api/src/functions/graphql.js
import {
createGraphQLHandler,
makeMergedSchema,
makeServices, // it's imported here
} from '@redwoodjs/api'
// ...
export const handler = createGraphQLHandler({
schema: makeMergedSchema({
schemas,
services: makeServices({ services }), // and gets called here
}),
db,
})
Since it gets called in makeMergedSchema
(above), you might think that it does the resolver-wrapping magic.
Actually, this is what makeServices
does:
/**
* This function is a stub for when we fully introduce the concept of
* services.
*/
export const makeServices: MakeServices = ({ services }) => {
return services
}
That’s right, it does nothing! Busted. (makeMergedSchema
does the resolver-wrapping magic). But, as the comment says, this function is just a stub for when we fully introduce the concept of Services.
Fully introduce the concept of Services? They’re not fully introduced yet? That was a surprise to me. And, I think, is the major source of Services confusion: we all want to know “how do I use Services”, but it’s hard to explain how to use something that isn’t fully introduced.
We’re saying fully introduced because Services are indeed a thing. They’re there in your app; they each have their own directory in api/src/services
; and if you name them just right, they get wrapped into your resolvers.
So it’s not like you can’t use them yet. Actually, far from it: you can already do pretty much anything you want with them. @peterp really gets at this in PR #334, where the auto import of db
was removed:
We feel that having the import visible in the services will help people understand that services are a layer where you can connect your schema to a database, but can also be a request to an API, to Redis, or anything else… the possibilities are endless.
So I’d be doing you a disservice if I said that you have to wait for Services to be fully introduced before you can start grokking them. The fact that the possibilities are endless is what makes Services so hard. Granted, we should have a golden example that we can point you to that says “this is how you do services”. But we could argue that we actually kind of already do: it’s called Prisma Client! (Nevertheless, tutorial + cookbook recipes + docs will come.)
Anyway, we all want to know what Services might look like when they’re fully introduced, so:
What Services might look like when they’re fully introduced
Quoting @peterp:
Our vision for Services is to provide a run-time middleware layer that can run before or after a service is executed.
Note that he said this in May, two months ago. We’re in alpha, we have ideas; things change! But here’s the example he provided of what a Services file might look like with this run-time middleware layer:
// api/src/services/todos.js
import { debugToConsole, printExecutionTimeInMs } from '@redwoodjs/services-middleware-debug'
import { verifyUserRole } from '@redwoodjs/services-auth-middleware'
export const before = {
deleteTodo: verifyUserRole('admin'),
}
export const after = {
'all': [debugToConsole, printExecutionTimeInMs]
}
export const deleteTodo = ({ id } ) => {
// delete from prisma
}
Again, this isn’t a spec; this is just an example. But hopefully it gets the idea across: like how Cells have before and after query lifecycle hooks, Services might have before and after execution lifecycle hooks.
How would this change auth?
Let’s look at how having a run-time middelware layer would change auth. We already kind of hinted at in the example above, but let’s make the before-after more explicit. Right now this is how we authenticate the api side:
export const createPost = ({ input }) => {
requireAuth()
return db.post.create({
data: input,
})
}
The check for authentication is part of the createPost
logic. If we had a run-time middleware layer, we could do something like:
export const before = {
createPost: verifyUserRole('admin'),
}
export const createPost = ({ input }) => {
return db.post.create({
data: input,
})
}
Now our business logic is even more encapsulated!
Note that the Authentication section of the Tutorial kind of gets at this already, but about the GraphQL interface:
Note that we’re putting the authentication checks in the service and not checking in the GraphQL interface (in the SDL files).
Redwood created the concept of services as containers for your business logic which can be used by other parts of your application besides the GraphQL API. By putting authentication checks here you can be sure that any other code that tries to create/update/delete a post will fall under the same authentication checks. In fact, Apollo (the GraphQL library Redwood uses) agrees with us!
With the run-time middleware layer, we’d be walking it back even further: authentication checks would be in the run-time middleware layer instead of in the Services’ functions directly.
GraphQL and the Business Logic Layer
With Services, you’ll hear the words “Business Logic” a lot. When it comes to GraphQL, it’s a very important set of words. If you’ve read the GraphQL docs, you might already know what I mean. Take a look at this figure:
This is from the GraphQL docs, so not every colored rectangle here is relevant to Jamstack architecture. Here is a better version (sorry that they’re not the same width; ignore that detail):
The point here is there shouldn’t be any business logic in your GraphQL. Redwood already makes this separation ridiculously easy.
But now the issue most of us are facing is how to think about Services as an API for our API. In other words, a lot is actually going on in that big blue rectangle:
And if we’re being accurate, we should include Prisma somewhere (here, the purple rectangles).
Above is an example API with a Billing Service and a User Service. Note that the Billing Service subsumes more than one table (the ellipsis should be filled in, but I just didn’t know with what). Read this figure from top to bottom; for example,
- The GraphQL Function receives a POST; say it’s a mutation, to bill a user
- It says to the Billing Service “bill user x”
- The Billing Service talks to the User Service, maybe saying something like “I need user x” (This detail is important, the Billing Service is just talking to the User Service, not the User Table)
- Both Services make the necessary changes to their underlying data tables (via the Prisma Client)
I glossed over infinitely many details there, but I didn’t think having all the details should stop me from giving a high-level overview. Hope this was helpful + feedback is greatly appreciated!