Pro Yearly is on sale from $80 to $50! »

Making an opinionated Web framework

Ba655e3712aaabfbca289fe136f85fe4?s=47 Masaki Hara
October 26, 2019

Making an opinionated Web framework

Some Web frameworks such as Ruby on Rails intentionally force a specific software architecture and, as a result, called opinionated frameworks. Opinionated frameworks have several advantages: a standardized directory layout helps newbies to understand the codebase. They are also designed to guide programmers to adapt the program to the corresponding software architecture. So the codebase will be more tolerant of rapid growth. I think that such an opinionated Web framework written in Rust would accelerate Rust adoption in the Web programming area. I'm attempting to make it by myself, using existing Web frameworks as a reference. In this talk, I'd like to describe how I thought and learned during design and implementation.

Ba655e3712aaabfbca289fe136f85fe4?s=128

Masaki Hara

October 26, 2019
Tweet

Transcript

  1. 2019/10/26 “Making an opinionated Web framework” 1 Making an opinionated

    Web framework Masaki Hara (Software Engineer at Wantedly) 2019/10/26 @ Rust.Tokyo 2019
  2. 2019/10/26 “Making an opinionated Web framework” 2 Myself • Software

    Engineer at Wantedly • Ruby on Rails and Golang • Wantedly People (Business card scanner & SNS) • Current interest: Rust • Former interests: formal methods, competitive programming, math, ...
  3. 2019/10/26 “Making an opinionated Web framework” 3 Part I: Before

    designing WAF • Background: my Rust interests • Background: my jobs • Single-feature servers • Domain servers • Why WAF now? • Our prototype Briefly describes why I started building WAF.
  4. 2019/10/26 “Making an opinionated Web framework” 4 Part II: Implementation

    • Example app: “conduit” • Async ecosystems • Structure • Nightly • Derive macros • Basic HTTP Tools • Diesel async wrapper • Overall progress Shows how our (partial) implementation is going.
  5. 2019/10/26 “Making an opinionated Web framework” 5 Part III: Design

    Questions • Schema-first development • Error handling • Configuration and DI • Crate splitting • Other questions raised Enumerates questions brought up during WAF design and our preliminary decisions.
  6. 2019/10/26 “Making an opinionated Web framework” 6 Part I: Before

    designing WAF
  7. 2019/10/26 “Making an opinionated Web framework” 7 Background: My Rust

    Interests • Language specs, features, and tricks Part I > Background
  8. 2019/10/26 “Making an opinionated Web framework” 8 Background: My Jobs

    • Backend: microservices written mostly in Rails and Golang • Performance is important but not critical Part I > Background
  9. 2019/10/26 “Making an opinionated Web framework” 9 Background: My Jobs

    • How can Rust perform better in our purpose? Part I > Background
  10. 2019/10/26 “Making an opinionated Web framework” 10 Single-feature servers •

    Very simple tasks that can be extracted to a microservice “furiganer” “原 将己” “はら まさき” Part I > Single-feature servers
  11. 2019/10/26 “Making an opinionated Web framework” 11 Single-feature servers •

    We already have one in Rust “refine-image-rust” S3 A bit context here: our service allows user to scan business cards. Part I > Single-feature servers
  12. 2019/10/26 “Making an opinionated Web framework” 12 Domain servers •

    A server with a single interest and multiple operations “users-rails” Register Get user info List users Update user info Part I > Domain servers
  13. 2019/10/26 “Making an opinionated Web framework” 13 A pair of

    domain servers • Servers to deliver a timeline of SNS contents “yashima-updates” “yashima-updates- contents” Each content A timeline A bit context here: our service has SNS feature based on the users’ business cards. Part I > Domain servers
  14. 2019/10/26 “Making an opinionated Web framework” 14 A pair of

    domain servers • Servers to deliver a timeline of SNS contents “yashima-updates” “yashima-updates- contents” Each content A timeline Fast simple delivery Easy implementation of complex business logics Part I > Domain servers
  15. 2019/10/26 “Making an opinionated Web framework” 15 A pair of

    domain servers • This split was well-designed and is working well. • However, there’s still a pain in the split; we often have business requirements which spans both servers. • What if there’s a language which is fast enough, scales well, and can handle complex business logics with reusable combinators…? Part I > Domain servers
  16. 2019/10/26 “Making an opinionated Web framework” 16 A pair of

    domain servers • This split was well-designed and is working well. • However, there’s still a pain in the split; we often have business requirements which spans both servers. • What is there’s a language which is fast enough, scales well, and can handle complex business logics with reusable combinators…? There is, but we need the ecosystem Part I > Domain servers
  17. 2019/10/26 “Making an opinionated Web framework” 17 Then, why WAF

    now? •…because we have the largest turning point in the Rust webserver ecosystem. Part I > Why WAF now?
  18. 2019/10/26 “Making an opinionated Web framework” 18 Quote from diesel

    async issue… • On 2018-01-13, @sgrif said… “Neither tokio nor futures have stable APIs at the moment” “It's virtually impossible to have anything that isn't 'static.” The lack of borrowing awaits has been blocking the whole async ecosystem! Part I > Why WAF now?
  19. 2019/10/26 “Making an opinionated Web framework” 19 We’ll experience a

    drastic development • async/await isn’t just a syntax sugar; this is the promising API for borrowing awaits, which didn’t exist before. • Many libraries can start designing their async API without sacrificing borrowing. • Competitions will be invoked by async/await release, and we may have “de facto standard” libraries for each genre. Part I > Why WAF now?
  20. 2019/10/26 “Making an opinionated Web framework” 20 Our prototype •See

    https://github.com/qnighy/nails •We’ll describe the progress in the next part Part I > Our prototype
  21. 2019/10/26 “Making an opinionated Web framework” 21 Part II: Implementation

  22. 2019/10/26 “Making an opinionated Web framework” 22 Example app: “conduit”

    • Same API, multiple implementations • Definitely useful for designing WAFs https://github.com/gothinkster/realworld Part II > Example app “conduit”
  23. 2019/10/26 “Making an opinionated Web framework” 23 Async ecosystems tokio

    • The battle-tested runtime and ecosystems rustasync + romio + juliex • New runtime and ecosystems made by the official working group Part II > Async ecosystems
  24. 2019/10/26 “Making an opinionated Web framework” 24 Async ecosystems tokio

    • The battle-tested runtime and ecosystems rustasync + romio + juliex • New runtime and ecosystems made by the official working group We once chose it (with some missing parts from tokio/hyper) Part II > Async ecosystems
  25. 2019/10/26 “Making an opinionated Web framework” 25 Rustasync fate •

    https://blog.rust-lang.org/2019/09/30/Async-await-hits- beta.html Part II > Async ecosystems
  26. 2019/10/26 “Making an opinionated Web framework” 26 Structure . lib

    nails nails_derive project_examples nails_realworld Cargo.toml Frameworks “Realworld” example Workspace manifest Part II > Structure
  27. 2019/10/26 “Making an opinionated Web framework” 27 Nightly for async/await

    • Pinned to the latest nightly with rustfmt, clippy, and rls. • Moved to 1.39.0 beta Part II > Nightly for async/await
  28. 2019/10/26 “Making an opinionated Web framework” 28 Derive macros •

    High priority because it’s tied to the core developer experience Part II > Derive macros
  29. 2019/10/26 “Making an opinionated Web framework” 29 Basic HTTP Tools

    These were necessary in the early stage of prototyping: • Routing • Parsing query strings • Parsing JSON bodies Part II > Basic HTTP Tools
  30. 2019/10/26 “Making an opinionated Web framework” 30 Diesel async wrapper

    • Diesel is a (supposedly most famous) type- safe query builder and ORM • Doesn’t support async yet • We’ll need the async wrapper eventually, but not for the prototyping Part II > Diesel async wrapper
  31. 2019/10/26 “Making an opinionated Web framework” 31 Overall progress •

    Not good ;( • ☑️ A derive macro for parsing requests • ☑️ Basic routing • ☑️ Error handling • ☑️ Async/await • ☐ Schema generation • ☐ Better ORM integration • ☐ Working webapp example • ☐ Middleware interface • ☐ Generators / scaffolding • ☐ Polish polish and polish… Part II > Overall progress
  32. 2019/10/26 “Making an opinionated Web framework” 32 Part III: Design

    Questions
  33. 2019/10/26 “Making an opinionated Web framework” 33 In this part…

    • As I said in the last page, our framework itself is totally under development. • However, we’re sure we already had good design questions and partial answers to these. • In this part, we focus on questions, rather than how I solved. Part III > In this part…
  34. 2019/10/26 “Making an opinionated Web framework” 34 Schema-first development •

    In a statically-typed language, we can give each endpoint a type. • We want to help clients (e.g. JS frontend, Android, iOS, …) build correct requests using these types. Part III > Schema-first
  35. 2019/10/26 “Making an opinionated Web framework” 35 Case I: gRPC

    and grpc-gateway • gRPC by itself is a schemaful solution, but it can also be transformed to a schemaful JSON sever using grpc-gateway. From https://github.com/grpc- ecosystem/grpc-gateway Part III > Schema-first
  36. 2019/10/26 “Making an opinionated Web framework” 36 Case I: gRPC

    and grpc-gateway “yashima-updates” “yashima-updates- contents” Each content A timeline This is grpc-gateway They interact using protobuf Part III > Schema-first
  37. 2019/10/26 “Making an opinionated Web framework” 37 Case I: gRPC

    and grpc-gateway • Protocol example Part III > Schema-first
  38. 2019/10/26 “Making an opinionated Web framework” 38 Case I: gRPC

    and grpc-gateway • Protocol example Part III > Schema-first
  39. 2019/10/26 “Making an opinionated Web framework” 39 Case I: gRPC

    and grpc-gateway • Generated codes Part III > Schema-first
  40. 2019/10/26 “Making an opinionated Web framework” 40 Case I: gRPC

    and grpc-gateway • Generated JSON schema (OpenAPI/Swagger) Part III > Schema-first
  41. 2019/10/26 “Making an opinionated Web framework” 41 Case I: gRPC

    and grpc-gateway • There are several implementations in Rust too https://github.com/hyperium/tonic https://github.com/stepancheg/grpc-rust https://github.com/tikv/grpc-rs Part III > Schema-first
  42. 2019/10/26 “Making an opinionated Web framework” 42 Case II: GraphQL

    • See this slide from my colleague ☺ https://speakerdeck.com/chloe463/graphqlsabafalsesukimahuasutokai-fa-woban-nian-jing-te Part III > Schema-first
  43. 2019/10/26 “Making an opinionated Web framework” 43 Case II: GraphQL

    • Famous Rust implementation https://github.com/graphql-rust/juniper Part III > Schema-first
  44. 2019/10/26 “Making an opinionated Web framework” 44 Case III: NestJS

    / FastAPI • Generate schemas from type annotations! Taken from https://fastapi.tiangolo.com/tutorial/body/ Part III > Schema-first
  45. 2019/10/26 “Making an opinionated Web framework” 45 Two directions •

    In our terms, there are “Rust-first” or “Strictly schema-first” approaches. grpc-gateway GraphQL / Apollo FastAPI protobuf Go Ruby OpenAPI GraphQL TypeScript Python OpenAPI Part III > Schema-first
  46. 2019/10/26 “Making an opinionated Web framework” 46 Schemas: our decision

    • Follow NestJS and FastAPI way: from Rust type to OpenAPI. • We can augment it with gRPC or GraphQL later. Part III > Schema-first
  47. 2019/10/26 “Making an opinionated Web framework” 47 Error handling •

    We always use Result<T, E>, but for E we have several choices. Trait Objects priv enum pub enum Box<dyn StdError> failure::Error pub enum MyError { IoError(…), … } tokio::timer::Error hyper::error::Error Part III > Error-handling
  48. 2019/10/26 “Making an opinionated Web framework” 48 Error handling: extra

    requirements •A framework must be able to turn errors into responses. •A framework usually has a default set of errors, but users may also want to define application-level errors. Part III > Error-handling
  49. 2019/10/26 “Making an opinionated Web framework” 49 Case I: actix-web

    • ResponseError trait does Web-specific things. • actix_web::Error works as Box<dyn ResponseError>.
  50. 2019/10/26 “Making an opinionated Web framework” 50 Case II: gotham

    • As of today, gotham::error is an alias for failure. • This is a good starting point before deciding how we finally handle errors.
  51. 2019/10/26 “Making an opinionated Web framework” 51 Errors and schemas

    • On one hand, we want to treat errors uniformly. • On the other hand, we want to describe errors precisely in the generated schemas. Different handlers return different errors. • Most frameworks sacrifice error preciseness (which seems basically rational)
  52. 2019/10/26 “Making an opinionated Web framework” 52 Error handling: our

    decision • A predefined enum NailsError, which is extendable by a trait ServiceError • Like trait objects, applications can arbitrarily add new errors. • An application is free to construct/inspect framework- defined errors.
  53. 2019/10/26 “Making an opinionated Web framework” 53 Error handling: our

    decision • Decouple error data and formatter using these interfaces: • Display implementation (private error message) • fn public_message(&self) (public error message) • fn class_name(&self) (error classification) • fn status(&self) (HTTP status code) • A formatter turns errors into responses. • Extract JSON schema from the formatter. • Optional runtime verification of error classes for better error description in the generated schema
  54. 2019/10/26 “Making an opinionated Web framework” 54 Configuration and DI

    •Configuration is something like DATABASE_URL, POOL_SIZE, or an actual pool of connections. •DI (Dependency Injection) is a replaceable/mockable component passed as a parameter.
  55. 2019/10/26 “Making an opinionated Web framework” 55 Configuration and DI

    •Configuration and DI are similar in that they’re globally shared parameters for the application. •I call them contexts in this presentation
  56. 2019/10/26 “Making an opinionated Web framework” 56 Configuration and DI

    •Due to its shared nature, it’s expected to be a (shallow-)cloneable object. •Configuration is a struct Arc<Config> •and DI is a trait object Arc<dyn S3>
  57. 2019/10/26 “Making an opinionated Web framework” 57 Typed vs. Untyped

    contexts Typed Contexts • Handler<Ctx> • Router<Ctx> • Middleware<Ctx> • Ctx is application-defined Untyped Contexts • Handler • Router • Middleware • We extract an application- defined Ctx from WAF- defined AnyCtx
  58. 2019/10/26 “Making an opinionated Web framework” 58 Cases: actix-web 0.7.x

    vs. 1.x actix-web 0.7.x • App<S> • HttpRequest<S> • FromRequest<S> • Middleware<S> • State<S> is the context extractor actix-web 1.x • App<T, B> • HttpRequest • FromRequest • Transform<S> (S isn’t a state) • Data<T> is the context extractor (no connection with T in App<T, B>)
  59. 2019/10/26 “Making an opinionated Web framework” 59 Really typed DI

    • Each injected dependency can have its own type parameter. • MyApp<D: DatabaseProvider, R: RedisLikeProvider, A: AuthorizationProvider> • I think for most cases, this is too verbose with relatively little performance and safety benefits.
  60. 2019/10/26 “Making an opinionated Web framework” 60 App and request

    contexts • Context may have states (with interior mutability). Mocked dependency usually has states. • Application-global states and request-local states are different. Instrumentations may want to store their statistics in both states. • How do we distinguish them?
  61. 2019/10/26 “Making an opinionated Web framework” 61 Context typedness: our

    decision • Use typed contexts. • Each application defines AppCtx and ReqCtx. • AppCtx + WAF’s request data → ReqCtx
  62. 2019/10/26 “Making an opinionated Web framework” 62 Context conversion: AsRef

    and Into • In a typed contexts setting, we sometimes need to convert between different contexts for better compositionality. • Question: AsRef or Into, which is better?
  63. 2019/10/26 “Making an opinionated Web framework” 63 Approach: AsContext •

    Similar to Into but clone-on-write. • Assume contexts to be always Clone. pub trait Context: std::fmt::Debug + Clone {} pub trait AsContext<U: Context>: Context { fn as_context(&self) -> Cow<'_, U>; } impl<T: Context> AsContext<T> for T { fn as_context(&self) -> Cow<'_, T> { Cow::Borrowed(self) } }
  64. 2019/10/26 “Making an opinionated Web framework” 64 Approach: AsContext •

    We only pay costs on context type boundary. • As flexible as Into.
  65. 2019/10/26 “Making an opinionated Web framework” 65 Crate splitting •

    We want to implement boilerplates (or generators) in the near future (but not now) • Directory structure is an important aspect of opinionated frameworks.
  66. 2019/10/26 “Making an opinionated Web framework” 66 Layered architecture variants

    • There are several variants of layered architecture. • They’re common in that they enforce one-way dependency of layers. Persistence Domain Model Application Presentation
  67. 2019/10/26 “Making an opinionated Web framework” 67 1 layer =

    1 crate? • In Rust world, one-way dependency is considered a good opportunity of crate splitting, but... app_presentation src Cargo.toml app_domain_service src Cargo.toml app_domain_model src Cargo.toml Isn’t this too verbose as a starting point?
  68. 2019/10/26 “Making an opinionated Web framework” 68 1 layer =

    1 crate? • In Rust world, one-way dependency is considered a good opportunity of crate splitting, but... src app_application Cargo.toml app_domain_service Cargo.toml app_domain_model Cargo.toml This requires extra stanza in Cargo.toml
  69. 2019/10/26 “Making an opinionated Web framework” 69 1 layer =

    1 crate? • In Rust world, one-way dependency is considered a good opportunity of crate splitting, but... src app_application Cargo.toml app_domain_service app_domain_model Mono-crate is concise and simple
  70. 2019/10/26 “Making an opinionated Web framework” 70 Crate splitting: our

    decision • I’m yet to decide this one… • We also have “hybrid” approach: • Initially, the generator generates a mono-crate tree. • At some stage, the programmer opt in to the splitted structure.
  71. 2019/10/26 “Making an opinionated Web framework” 71 Other questions raised

    • Generators / boilerplates / scaffolding • Middleware interfaces • Devserver and autoloading • Mocking • @asomers gave a great comparison and even made the greatest mocking library.
  72. 2019/10/26 “Making an opinionated Web framework” 72 Summary & Recap

    • Part I: we want to apply Rust for business-logic-heavy Web backends. • Part II: we started implementing WAF but it’s still a prototype. • Part III: there are interesting design questions about opinionated WAFs. Thanks!
  73. 2019/10/26 “Making an opinionated Web framework” 73 Recap: Part I:

    Before designing WAF • Background: my Rust interests • Background: my jobs • Single-feature servers • Domain servers • Why WAF now? • Our prototype Briefly describes why I started building WAF.
  74. 2019/10/26 “Making an opinionated Web framework” 74 Recap: Part II:

    Implementation • Example app: “conduit” • Async ecosystems • Structure • Nightly • Derive macros • Basic HTTP Tools • Diesel async wrapper • Overall progress Shows how our (partial) implementation is going.
  75. 2019/10/26 “Making an opinionated Web framework” 75 Recap: Part III:

    Design Questions • Schema-first development • Error handling • Configuration and DI • Crate splitting • Other questions raised Enumerates questions brought up during WAF design and our preliminary decisions.