diff --git a/ExpressAPI/src/managers/ExerciceManager.ts b/ExpressAPI/src/managers/ExerciceManager.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4938f726894a17deca54f837fa080b51096e653d
--- /dev/null
+++ b/ExpressAPI/src/managers/ExerciceManager.ts
@@ -0,0 +1,28 @@
+import Exercice from '../models/Exercice';
+
+
+class ExerciceManager {
+    private static _instance: ExerciceManager;
+
+    private constructor() { }
+
+    public static get instance(): ExerciceManager {
+        if ( !ExerciceManager._instance ) {
+            ExerciceManager._instance = new ExerciceManager();
+        }
+
+        return ExerciceManager._instance;
+    }
+
+    createObjectFromRawSql(raw: any): Exercice {
+        const exercice = Exercice.createFromSql(raw);
+
+        exercice.exerciceGitlabCreationInfo = raw.exerciceGitlabCreationInfo;
+        exercice.exerciceGitlabLastInfo = raw.exerciceGitlabLastInfo;
+
+        return exercice;
+    }
+}
+
+
+export default ExerciceManager.instance;
diff --git a/ExpressAPI/src/models/Exercice.ts b/ExpressAPI/src/models/Exercice.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d81b868538cbd6fe30c5164618f330d51218966a
--- /dev/null
+++ b/ExpressAPI/src/models/Exercice.ts
@@ -0,0 +1,98 @@
+import Model            from './Model';
+import db               from '../helpers/DatabaseHelper';
+import GitlabRepository from '../shared/types/Gitlab/GitlabRepository';
+
+
+class Exercice extends Model {
+    static tableName: string = 'Exercice';
+
+    exerciceId: string = '';
+    exerciceEnonceName: string = '';
+    exerciceName: string = '';
+    exerciceGitlabId: number = null;
+    exerciceGitlabLink: string = '';
+    private _exerciceGitlabCreationInfo: string = '{}';
+    private _exerciceGitlabLastInfo: string = '{}';
+    exerciceGitlabLastInfoTs: number = null;
+
+    get exerciceGitlabCreationInfo(): GitlabRepository {
+        return JSON.parse(this._exerciceGitlabCreationInfo);
+    }
+
+    set exerciceGitlabCreationInfo(value: any) {
+        if ( typeof value === 'string' ) {
+            this._exerciceGitlabCreationInfo = value;
+            return;
+        }
+
+        this._exerciceGitlabCreationInfo = JSON.stringify(value);
+    }
+
+    get exerciceGitlabLastInfo(): GitlabRepository {
+        return JSON.parse(this._exerciceGitlabLastInfo);
+    }
+
+    set exerciceGitlabLastInfo(value: any) {
+        if ( typeof value === 'string' ) {
+            this._exerciceGitlabLastInfo = value;
+            return;
+        }
+
+        this._exerciceGitlabLastInfo = JSON.stringify(value);
+    }
+
+    public async toJsonObject(): Promise<Object> {
+        const result = {
+            'id'                : this.exerciceId,
+            'enonceName'        : this.exerciceEnonceName,
+            'name'              : this.exerciceName,
+            'gitlabId'          : this.exerciceGitlabId,
+            'gitlabLink'        : this.exerciceGitlabLink,
+            'gitlabCreationInfo': this.exerciceGitlabCreationInfo,
+            'gitlabLastInfo'    : this.exerciceGitlabLastInfo,
+            'gitlabLastInfoTs'  : this.exerciceGitlabLastInfoTs
+        };
+
+        return result;
+    };
+
+    public importFromJsonObject(jsonObject: any) {
+        this.exerciceId = jsonObject.id;
+        this.exerciceEnonceName = jsonObject.enonceName;
+        this.exerciceName = jsonObject.name;
+        this.exerciceGitlabId = jsonObject.gitlabId;
+        this.exerciceGitlabLink = jsonObject.gitlabLink;
+        this.exerciceGitlabCreationInfo = jsonObject.gitlabCreationInfo;
+        this.exerciceGitlabLastInfo = jsonObject.gitlabLastInfo;
+        this.exerciceGitlabLastInfoTs = jsonObject.gitlabLastInfoTs;
+    }
+
+    public toDb(): any {
+        return {
+            exerciceId                : this.exerciceId,
+            exerciceEnonceName        : this.exerciceEnonceName,
+            exerciceName              : this.exerciceName,
+            exerciceGitlabId          : this.exerciceGitlabId,
+            exerciceGitlabLink        : this.exerciceGitlabLink,
+            exerciceGitlabCreationInfo: this._exerciceGitlabCreationInfo,
+            exerciceGitlabLastInfo    : this._exerciceGitlabLastInfo,
+            exerciceGitlabLastInfoTs  : this.exerciceGitlabLastInfoTs
+        };
+    }
+
+    async create(): Promise<Exercice> {
+        await db(Exercice.tableName).insert(this.toDb());
+        return this;
+    }
+
+    update(): Promise<void> {
+        return db(Exercice.tableName).where('exerciceId', this.exerciceId).update(this.toDb());
+    }
+
+    del(): Promise<void> {
+        return db(Exercice.tableName).where('exerciceId', this.exerciceId).del();
+    }
+}
+
+
+export default Exercice;
diff --git a/ExpressAPI/src/models/ExerciceMember.ts b/ExpressAPI/src/models/ExerciceMember.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cf2a2c8ee71d85c060b4e420e3b436917403e6b4
--- /dev/null
+++ b/ExpressAPI/src/models/ExerciceMember.ts
@@ -0,0 +1,43 @@
+import Model from './Model';
+import db    from '../helpers/DatabaseHelper';
+
+
+class ExerciceMember extends Model {
+    static tableName: string = 'ExerciceMember';
+
+    exerciceId: string = '';
+    userId: number = null;
+
+    public async toJsonObject(): Promise<Object> {
+        const result = {
+            'exerciceId': this.exerciceId,
+            'userId'    : this.userId
+        };
+
+        return result;
+    };
+
+    public importFromJsonObject(jsonObject: any) {
+        this.exerciceId = jsonObject.exerciceId;
+        this.userId = jsonObject.userId;
+    }
+
+    public toDb(): any {
+        return {
+            exerciceId: this.exerciceId,
+            userId    : this.userId
+        };
+    }
+
+    async create(): Promise<ExerciceMember> {
+        await db(ExerciceMember.tableName).insert(this.toDb());
+        return this;
+    }
+
+    del(): Promise<void> {
+        return db(ExerciceMember.tableName).where('exerciceId', this.exerciceId).andWhere('userId', this.userId).del();
+    }
+}
+
+
+export default ExerciceMember;
diff --git a/ExpressAPI/src/routes/ApiRoutesManager.ts b/ExpressAPI/src/routes/ApiRoutesManager.ts
index 85ec024f873bfcf0b6946e90781fb45031fc60f0..f45b5cf3fd3f65a869098440d3ec85b71725f90f 100644
--- a/ExpressAPI/src/routes/ApiRoutesManager.ts
+++ b/ExpressAPI/src/routes/ApiRoutesManager.ts
@@ -1,9 +1,10 @@
-import { Express }   from 'express-serve-static-core';
-import RoutesManager from '../express/RoutesManager';
-import BaseRoutes    from './BaseRoutes';
-import SessionRoutes from './SessionRoutes';
-import EnonceRoutes  from './EnonceRoutes';
-import GitlabRoutes  from './GitlabRoutes';
+import { Express }    from 'express-serve-static-core';
+import RoutesManager  from '../express/RoutesManager';
+import BaseRoutes     from './BaseRoutes';
+import SessionRoutes  from './SessionRoutes';
+import EnonceRoutes   from './EnonceRoutes';
+import GitlabRoutes   from './GitlabRoutes';
+import ExerciceRoutes from './ExerciceRoutes';
 
 
 class AdminRoutesManager implements RoutesManager {
@@ -22,8 +23,9 @@ class AdminRoutesManager implements RoutesManager {
     registerOnBackend(backend: Express) {
         BaseRoutes.registerOnBackend(backend);
         SessionRoutes.registerOnBackend(backend);
-        EnonceRoutes.registerOnBackend(backend);
         GitlabRoutes.registerOnBackend(backend);
+        EnonceRoutes.registerOnBackend(backend);
+        ExerciceRoutes.registerOnBackend(backend);
     }
 }
 
diff --git a/ExpressAPI/src/routes/ExerciceRoutes.ts b/ExpressAPI/src/routes/ExerciceRoutes.ts
new file mode 100644
index 0000000000000000000000000000000000000000..418ec2fd10ba58c1e7a20632fd4f5333155564cb
--- /dev/null
+++ b/ExpressAPI/src/routes/ExerciceRoutes.ts
@@ -0,0 +1,129 @@
+import { Express }               from 'express-serve-static-core';
+import express                   from 'express';
+import * as ExpressValidator     from 'express-validator';
+import { StatusCodes }           from 'http-status-codes';
+import RoutesManager             from '../express/RoutesManager';
+import ParamsValidatorMiddleware from '../middlewares/ParamsValidatorMiddleware';
+import ApiRequest                from '../models/ApiRequest';
+import SecurityMiddleware        from '../middlewares/SecurityMiddleware';
+import GitlabUser                from '../shared/types/Gitlab/GitlabUser';
+import GitlabManager             from '../managers/GitlabManager';
+import Config                    from '../config/Config';
+import GitlabRepository          from '../shared/types/Gitlab/GitlabRepository';
+import Enonce                    from '../models/Enonce';
+import { AxiosError }            from 'axios';
+import logger                    from '../shared/logging/WinstonLogger';
+import DojoValidators            from '../helpers/DojoValidators';
+import { v4 as uuidv4 }          from 'uuid';
+import GitlabMember              from '../shared/types/Gitlab/GitlabMember';
+import GitlabAccessLevel         from '../shared/types/Gitlab/GitlabAccessLevel';
+import User                      from '../models/User';
+import UserManager               from '../managers/UserManager';
+import Exercice                  from '../models/Exercice';
+import ExerciceMember            from '../models/ExerciceMember';
+import ExerciceManager           from '../managers/ExerciceManager';
+
+
+class ExerciceRoutes implements RoutesManager {
+    private static _instance: ExerciceRoutes;
+
+    private constructor() { }
+
+    public static get instance(): ExerciceRoutes {
+        if ( !ExerciceRoutes._instance ) {
+            ExerciceRoutes._instance = new ExerciceRoutes();
+        }
+
+        return ExerciceRoutes._instance;
+    }
+
+    private readonly exerciceValidator: ExpressValidator.Schema = {
+        members: {
+            trim           : true,
+            notEmpty       : true,
+            customSanitizer: DojoValidators.jsonSanitizer
+        }
+    };
+
+    registerOnBackend(backend: Express) {
+        backend.post('/enonces/:enonceNameOrUrl/exercices', SecurityMiddleware.check(true), ParamsValidatorMiddleware.validate(this.exerciceValidator), this.createExercice.bind(this));
+    }
+
+    private getExerciceName(enonce: Enonce, members: Array<GitlabUser>, suffix: number): string {
+        return `DojoEx - ${ enonce.enonceName } - ${ members.map(member => member.username).join(' + ') }${ suffix > 0 ? ` - ${ suffix }` : '' }`;
+    }
+
+    private getExercicePath(enonce: Enonce, exerciceId: string): string {
+        return `dojo-ex_${ enonce.enonceGitlabLastInfo.path }_${ exerciceId }`;
+    }
+
+    private async createExercice(req: ApiRequest, res: express.Response) {
+        const params: { members: Array<GitlabUser> } = req.body;
+        params.members = [ await req.session.profile.gitlabProfile.value, ...params.members ];
+
+        const exerciceId: string = uuidv4();
+
+        let repository: GitlabRepository;
+        let suffix: number = 0;
+        do {
+            try {
+                repository = await GitlabManager.forkRepository(req.boundParams.enonce.enonceGitlabCreationInfo.id, this.getExerciceName(req.boundParams.enonce, params.members, suffix), this.getExercicePath(req.boundParams.enonce, exerciceId), Config.exercice.default.description.replace('{{ENONCE_NAME}}', req.boundParams.enonce.enonceName), Config.exercice.default.visibility, Config.gitlab.group.exercices);
+                break;
+            } catch ( error ) {
+                if ( error instanceof AxiosError ) {
+                    if ( error.response.data.message.name && error.response.data.message.name == 'has already been taken' ) {
+                        suffix++;
+                    } else {
+                        return res.status(error.response.status).send();
+                    }
+                } else {
+                    return res.status(StatusCodes.INTERNAL_SERVER_ERROR).send();
+                }
+            }
+        } while ( suffix < Config.exercice.maxSameName );
+
+        if ( suffix >= Config.exercice.maxSameName ) {
+            return res.status(StatusCodes.INSUFFICIENT_SPACE_ON_RESOURCE).send();
+        }
+
+        try {
+            const members: Array<GitlabMember | false> = await Promise.all([ ...new Set([ ...(await req.boundParams.enonce.staff.value).map(member => member.userGitlabId), ...params.members.map(member => member.id) ]) ].map(async (memberId: number): Promise<GitlabMember | false> => {
+                try {
+                    return await GitlabManager.addRepositoryMember(repository.id, memberId, GitlabAccessLevel.Maintainer);
+                } catch ( e ) {
+                    return false;
+                }
+            }));
+
+            const exercice: Exercice = await ExerciceManager.createObjectFromRawSql({
+                                                                                        exerciceId                : exerciceId,
+                                                                                        exerciceEnonceName        : req.boundParams.enonce.enonceName,
+                                                                                        exerciceName              : repository.name,
+                                                                                        exerciceGitlabId          : repository.id,
+                                                                                        exerciceGitlabLink        : repository.web_url,
+                                                                                        exerciceGitlabCreationInfo: JSON.stringify(repository),
+                                                                                        exerciceGitlabLastInfo    : JSON.stringify(repository),
+                                                                                        exerciceGitlabLastInfoTs  : new Date().getTime()
+                                                                                    }).create();
+
+            let dojoUsers: Array<User> = await UserManager.getFromGitlabUsers(params.members, true) as Array<User>;
+            dojoUsers = dojoUsers.reduce((unique, user) => (unique.findIndex(uniqueUser => uniqueUser.userId === user.userId) !== -1 ? unique : [ ...unique, user ]), Array<User>());
+            await Promise.all(dojoUsers.map(dojoUser => ExerciceMember.createFromSql({
+                                                                                         exerciceId: exercice.exerciceId,
+                                                                                         userId    : dojoUser.userId
+                                                                                     }).create()));
+
+            return req.session.sendResponse(res, StatusCodes.OK, exercice.toJsonObject());
+        } catch ( error ) {
+            if ( error instanceof AxiosError ) {
+                return res.status(error.response.status).send();
+            }
+
+            logger.error(error);
+            return res.status(StatusCodes.INTERNAL_SERVER_ERROR).send();
+        }
+    }
+}
+
+
+export default ExerciceRoutes.instance;