Version 3.0 of Foal is finally there!
It's been a long work and I'm excited to share with you the new features of the framework 🎉 . The upgrading guide can be found here.
Here are the new features and improvements of version 3!
For those who don't know it yet, Foal is a 5-year-old full-featured Node.JS framework for building web applications. It is simple and easy to use, based on TypeScript with very detailed documentation. And the best part: it is tested by more than 2,000 tests.
Full support of TypeORM v0.3
TypeORM v0.3 provides greater typing safety and this is something that will be appreciated when moving to the new version of Foal.
The version 0.3 of TypeORM has a lot of changes compared to the version 0.2 though. Features such as the ormconfig.json
file have been removed and functions such as createConnection
, getManager
or getRepository
have been deprecated.
A lot of work has been done to make sure that @foal/typeorm
, new projects generated by the CLI and examples in the documentation use version 0.3 of TypeORM without relying on deprecated functions or patterns.
In particular, the connection to the database is now managed by a file src/db.ts
that replaces the older ormconfig.json
.
For those new to Foal, TypeORM is the default ORM used in all new projects. But you can use any other ORM or query builder if you want, as the core framework is ORM independent.
Code simplified
Some parts of the framework have been simplified to require less code and make it more understandable.
Authentication
The @UseSessions
and @JWTRequired
authentication hooks called obscure functions such as fetchUser
, fetchUserWithPermissions
to populate the ctx.user
property. The real role of these functions was not clear and a newcomer to the framework could wonder what they were for.
This is why these functions have been removed and replaced by direct calls to database models.
// Version 2
@UseSessions({ user: fetchUser(User) })
@JWTRequired({ user: fetchUserWithPermissions(User) })
// Version 3
@UseSessions({ user: (id: number) => User.findOneBy({ id }) })
@JWTRequired({ user: (id: number) => User.findOneWithPermissionsBy({ id }) })
File upload
When uploading files in a multipart/form-data request, it was not allowed to pass optional fields. This is now possible.
The interface of the @ValidateMultipartFormDataBody
hook, renamed to @ParseAndValidateFiles
to be more understandable for people who don't know the HTTP protocol handling the upload, has been simplified.
Examples with only files
// Version 2
@ValidateMultipartFormDataBody({
files: {
profile: { required: true }
}
})
// Version 3
@ParseAndValidateFiles({
profile: { required: true }
})
Examples with files and fields
// Version 2
@ValidateMultipartFormDataBody({
files: {
profile: { required: true }
}
fields: {
description: { type: 'string' }
}
})
// Version 3
@ParseAndValidateFiles(
{
profile: { required: true }
},
// The second parameter is optional
// and is used to add fields. It expects an AJV object.
{
type: 'object',
properties: {
description: { type: 'string' }
},
required: ['description'],
additionalProperties: false
}
)
Database models
Using functions like getRepository
or getManager
to manipulate data in a database is not necessarily obvious to newcomers. It adds complexity that is not necessary for small or medium sized projects. Most frameworks prefer to use the Active Record pattern for simplicity.
This is why, from version 3 and to take into account that TypeORM v0.3 no longer uses a global connection, the examples in the documentation and the generators will extend all the models from BaseEntity
. Of course, it will still be possible to use the functions below if desired.
// Version 2
@Entity()
class User {}
const user = getRepository(User).create();
await getRepository(User).save(user);
// Version 3
@Entity()
class User extends BaseEntity {}
const user = new User();
await user.save();
Better typing
The use of TypeScript types has been improved and some parts of the framework ensure better type safety.
Validation with AJV
Foal's version uses ajv@8
which allows you to bind a TypeScript type with a JSON schema object. To do this, you can import the generic type JSONSchemaType
to build the interface of the schema object.
import { JSONSchemaType } from 'ajv';
interface MyData {
foo: number;
bar?: string
}
const schema: JSONSchemaType<MyData> = {
type: 'object',
properties: {
foo: { type: 'integer' },
bar: { type: 'string', nullable: true }
},
required: ['foo'],
additionalProperties: false
}
File upload
In version 2, handling file uploads in the controller was tedious because all types were any
. Starting with version 3, it is no longer necessary to cast the types to File
or File[]
:
// Version 2
const name = ctx.request.body.fields.name;
const file = ctx.request.body.files.avatar as File;
const files = ctx.request.body.files.images as File[];
// After
const name = ctx.request.body.name;
// file is of type "File"
const file = ctx.files.get('avatar')[0];
// files is of type "Files"
const files = ctx.files.get('images');
Authentication
In version 2, the user
option of @UseSessions
and @JWTRequired
expected a function with this signature:
(id: string|number, services: ServiceManager) => Promise<any>;
There was no way to guess and guarantee the type of the user ID and the function had to check and convert the type itself if necessary.
The returned type was also very permissive (type any
) preventing us from detecting silly errors such as confusion between null
and undefined
values.
In version 3, the hooks have been added a new userIdType
option to check and convert the JavaScript type if necessary and force the TypeScript type of the function. The returned type is also safer and corresponds to the type of ctx.user
which is no longer any
but { [key : string] : any } | null
.
Example where the ID is a string
@JWTRequired({
user: (id: string) => User.findOneBy({ id });
userIdType: 'string',
})
Example where the ID is a number
@JWTRequired({
user: (id: number) => User.findOneBy({ id });
userIdType: 'number',
})
By default, the value of userIdType
is a number, so we can simply write this:
@JWTRequired({
user: (id: number) => User.findOneBy({ id });
})
GraphQL
In version 2, GraphQL schemas were of type any
. In version 3, they are all based on the GraphQLSchema
interface.
Closer to JS ecosystem standards
Some parts have been modified to get closer to the JS ecosystem standards. In particular:
Development command
The npm run develop
has been renamed to npm run dev
.
Configuration through environment variables
When two values of the same variable are provided by a .env
file and an environment variable, then the value of the environment is used (the behavior is similar to that of the dotenv library).
null
vs undefined
values
When the request has no session or the user is not authenticated, the values of ctx.session
and ctx.user
are null
and no longer undefined
. This makes sense from a semantic point of view, and it also simplifies the user assignment from the find
functions of popular ORMs (Prisma, TypeORM, Mikro-ORM). They all return null
when no value is found.
More open to other ORMs
TypeORM is the default ORM used in the documentation examples and in the projects generated by the CLI. But it is quite possible to use another ORM or query generator with Foal. For example, the authentication system (with sessions or JWT) makes no assumptions about database access.
Some parts of the framework were still a bit tied to TypeORM in version 2. Version 3 fixed this.
Shell scripts
When running the foal generate script
command, the generated script file no longer contains TypeORM code.
Permission system
The @PermissionRequired
option is no longer bound to TypeORM and can be used with any ctx.user
that implements the IUserWithPermissions
interface.
Smaller AWS S3 package
The @foal/aws-s3
package is now based on version 3 of the AWS SDK. Thanks to this, the size of the node_modules
has been reduced by three.
Dependencies updated and support of Node latest versions
All Foal's dependencies have been upgraded. The framework is also tested on Node versions 16 and 18.
Some bug fixes
If the configuration file production.js
explicitly returns undefined
for a given key and the default.json
file returns a defined value for this key, then the value from the default.json
file is returned by Config.get
.
Article originally published here.
Top comments (0)