Implementing Fernand backend with Sanic

Hi! I'm Cyril Nicodeme I am a French web developer passionate in building services. My favorite stack is Python (Sanic) with VueJS.
I founded Reflectiv with my friend, Antoine Minoux, and we've built various online services. From there, we built, grew and sold VoilaNorbert.com, Transferslot.com and ImprovMX.com.
I'm now focusing my time on PDFShift - an HTML to PDF API conversion and Fernand - a SaaS-focused customer support.
Feel free to contact me to talk about anything! Happy to chat!
Now that Fernand is out, it's time for me to reflect back on how I spent the last few years (yes, years), working on this crazy project.
Just to clarify things, Fernand is our latest project, a Help Desk SaaS with a focus on speed, efficiency, and calm. We were just two working on this, Antoine and me. Antoine is the designer and marketing specialist behind the voice of Fernand, and I'm the technical one.
This article is about the evolution of the technical side.
Fernand first started being built on Flask. I've been building all my recent projects using Flask and I've a pretty good grasp of how it works. ImprovMX, our other service, is running on Flask with a few modified internals specific to our needs. So it made sense to build Fernand using Flask too.
But while we were heads down on building it, we had a few issues on ImprovMX that was caused because some requests would take too long to complete. The issue is that when Flask accepts a request, it will block a thread for this request until it is completed. if that requests fetch a lot of data from the database and takes time, the thread won't be free for other users while waiting for the database results.
At that time, I was curiously checking the landscape to see if there weren't anything new I could use on Fernand rather than staying with the old (but reliable) Flask. I had seen FastAPI, Sanic, Starlette and a few others, and tried FastAPI and Sanic. What was interesting me was the A in ASGI: Asynchronous Server Gateway Interface. I had a clear vision that this would become handy, moreover because recent Python versions introduced the async keyword. It made sense.
After the downtime we had at ImprovMX, and once I discovered it was because some endpoints were slow (yes, I fixed them to improve their response time), it made sense for me to move over an ASGI framework.
Sanic was appealing to me. Firstly because it was tested as one of the fastest Python framework, but also because the way the codebase, and how the flow was done, was more how I think.
So here I was, migrating the whole Flask codebase to Sanic.
I'm not a fan of adding decorators on top of decorators, so having a :
@ratelimit(5)
@authenticated
@db
@app.get('/')
async def show(request):
limit = request.args.get('limit', 20)
return sanic.response.json(await MyModel.get_latest(limit))
Was absolutely not appealing to me.
So I rewrote how the routes were handled, and implemented a few ideas from FastAPI along the way. Now, you could do this :
@app.get('/', ratelimit=5, auth=True)
async def show(user, limit:int = 20):
return await MyModel.get_latest(limit)
Way more cooler isn't it?
A few things to note from this:
There are no more "lots of decorators", all the properties are defined in the route ("@app.get"), which makes more sense - at least for me
The route doesn't impose the
requestparameter anymore, and you can have the user parameter from the auth parameter, but it's optional (if you don't ask it, it won't be added in the function)The request parameter is added if asked, and cast to the proper type if type hint is provided
The response is adjusted based on what is returned. I went a bit beyond what can be seen here, because it's possible to return a Model object, and it will be serialized based on more advanced code.
These two decisions, from using Sanic as the main framework, and rewriting the router to handle more parameter is the best decision we could have for a clear code base. I’m really happy with how things turned out :)
